:root {
  /* "Gilded & Obsidian" — deep warm obsidian blacks with champagne-gold
     accents in place of the old electric SaaS blue. The warmth is subtle
     (a few degrees toward brown, not sepia); the gold accent ties the
     chrome to the app's existing wealth identity (gold buys, favorites,
     Members) instead of fighting it. */
  --bg-base: #0C0B09;
  --bg-chart: #131110;
  --bg-panel: #1B1815;
  --bg-input: #282320;
  --border-main: #2B2621;
  --border-light: #3C352B;
  --border-active: #C9A64D;
  /* Structural panels (top bar, chart, sidebar, rating box) already read as
     "raised" against --bg-base from background contrast alone — the old
     --border-main outline on top of that was redundant, and with borders on
     every single panel nothing got to look "active." This near-invisible
     hairline replaces those outlines; real borders are now reserved for
     things that are actually selected/active (Target Sell when it's the
     active side, a highlighted favorites row, etc). */
  --border-hair: rgba(255, 255, 255, 0.05);
  --accent-bright: #E5C158;
  --accent-ink: #1C1300;
  --text-main: #D9D3C7;
  --text-muted: #8A8274;
  /* Structural chrome — section headers and the like. Deliberately NOT an
     accent: a header's job is to name a region, and when every one of them
     was gold the colour stopped meaning "signal" anywhere on the page. The
     section's identity still shows, in the 3px accent stripe beside it.
     Muted parchment, not the cool slate (#8A8D9B) this was. That slate was
     the only blue-leaning hue on a page of warm obsidian and gold, so five
     headers in it read as imported chrome sitting on top of the UI rather
     than part of it. This is the same VALUE — measured at 5.2:1 on the panel
     against the slate's 5.4:1, so it stays exactly as secondary as it was —
     just turned warm. It has to stay a value match: the whole point of this
     token is that headers sit under the data they label. */
  --text-structural: #948A78;
  --buy-color: #E5B842;
  --sell-color: #26A9AB;
  --vol-buy: rgba(229, 184, 66, 0.35);
  --vol-sell: rgba(38, 169, 171, 0.35);
  --positive: #10B981;
  --negative: #EF5350;
  --fav-gold: #FFB300;
  --shadow: rgba(0, 0, 0, 0.5);
  /* RuneScape accent palette — used for favorite highlights and headline
     callouts. Picked to read as "in-game GE" without overwhelming the dark
     trading-style chrome.
     THREE roles, one hue each. Gold is the buy side, teal is the sell side,
     and green means profit or opportunity — never a side. Sell was briefly
     merged into green, which did remove a near-duplicate but cost the thing
     that mattered more: with highs, Target Sell AND every positive number
     all green, "green" stopped saying which of those it meant, and Target
     Buy vs Target Sell no longer separated at a glance. Teal earns its keep
     by being the one colour that only ever means "sell side".
     The teal is pulled COOL, but only just past neutral. Three points on
     that axis got tested and the middle one won:

       #26A69A  hue 174  green-leaning teal   dE 27 from the profit green
       #26A9AB  hue 181  green == blue        dE 35   <- this one
       #22A5B8  hue 188  blue-leaning cyan    dE 45

     174 is too close: fourteen degrees is not enough separation for two
     colours that land in the same glance (a green profit figure beside a
     teal Target Sell, teal high dots over a green +change). 188 separates
     cleanly but goes dim and cold on this background — blue carries almost
     no luminance, so a bluer teal at the same computed brightness reads
     weaker, and it stops feeling native beside the gold. 181 sits where the
     green and blue channels are within two points of each other: still
     unmistakably not the profit green, still warm enough to hold up on
     obsidian.
     Luminance is +5.7% on the original, and that is the one number that had
     to move UP rather than stay put — it buys back the crispness the bluer
     version lost. The chart repaints ~289 high dots a frame in this colour
     and their intensity has been tuned three separate times, so treat that
     figure as a ceiling, not a starting point.
     If this is ever retuned: stay inside hue 178-184. Below is the
     too-close-to-green failure, above is the cold-and-dim one.
     The two greens left are split by JOB, not taste: --rs-green is the
     bright signal green for things that are shouting (a 5-day-high badge, a
     row accent, a Prime Flip band), and --rs-green-deep is the same emerald
     as --positive, for green that is just stating a number (profit figures,
     a live dot, a fresh timestamp). Keep it to those two — a third mid-green
     buys nothing and makes "which green means what" unanswerable again. */
  --rs-green: #4FFF8E;
  --rs-green-deep: #10B981;
  --rs-gold: #FFD24D;
  --rs-gold-deep: #C8841C;
  --rs-parchment: #F2D49B;
  /* ONE section-header recipe. Potential Profit, Analyst Rating,
     Recommended Flip, Favorites, Find Opportunities and the 30-Day Range
     meter are peers — sections of equal rank — but they had drifted to
     four different sizes
     (9 / 10.5 / 11 / 13px), two colours, two weights and three tracking
     values, with icons on only three of the five. Read top to bottom that
     looked like a hierarchy nobody designed. These tokens are the single
     source; change them here, not at the call sites.
     No leading glyphs: five 12px line icons stacked down a 330px column
     read as decoration competing with the accent stripe that already names
     each section, so the stripe does that job alone.
     700, not 800: at 11px uppercase with 0.7px tracking a label is already
     unmistakably a header, and at 800 these five were the heaviest text in
     the sidebar — heavier than the item names and prices they exist to
     label. On a 3x phone screen that overshoot is where "everything is
     bold" comes from; structural chrome should sit under its data, not on
     top of it. */
  --hdr-size: 11px;
  --hdr-weight: 700;
  --hdr-track: 0.7px;
}
* { box-sizing: border-box; }
/* Body is allowed to scroll naturally so the footer (with the suggest-a-
   feature link, FAQ jump, etc.) and the long-form About section below the
   app viewport are reachable. .wrap stays at 100dvh so the app dashboard
   fills the initial paint — anything below sits past the fold. */
html, body { width: 100%; }
body { margin: 0; min-height: 100dvh; background: var(--bg-base); color: var(--text-main); font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; }
button, input { touch-action: manipulation; }
/* Themed scrollbars — the browser default light/grey bars looked out of
   place on the dark UI. WebKit (Chrome / Safari / Edge / Brave / Opera)
   uses the ::-webkit-scrollbar pseudo-elements; Firefox uses the
   scrollbar-width / scrollbar-color properties. Mobile native scrollers
   ignore both, which is fine — they're invisible by default anyway. */
* { scrollbar-width: thin; scrollbar-color: var(--border-light) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 6px; border: 2px solid var(--bg-base); transition: background 0.15s ease; }
::-webkit-scrollbar-thumb:hover { background: var(--border-active); }
::-webkit-scrollbar-corner { background: transparent; }
/* Visually-hidden but readable to screen readers and search-engine crawlers.
   Used for the page H1 so the app keeps its dense dashboard layout without
   sacrificing semantic structure. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
/* About-PocketGE section — sits below the app viewport. Visible to anyone who
   scrolls past the dashboard, and crawler-indexable for organic search. */
.about-section { max-width: 880px; margin: 32px auto 64px; padding: 0 20px; color: var(--text-muted); font-size: 13px; line-height: 1.65; }
/* The SEO/about content is thousands of laid-out nodes below the fold —
   skip its render work until scrolled near (crawlers still see it all).
   Big intrinsic-size placeholder keeps the scrollbar honest. */
.about-section, .site-footer { content-visibility: auto; contain-intrinsic-size: auto 3000px; }
.about-section h2 { color: var(--text-main); font-size: 18px; margin: 28px 0 8px; font-weight: 700; }
.about-section h2:first-of-type { margin-top: 0; }
.about-section p { margin: 0 0 12px; }
.about-section a { color: var(--border-active); text-decoration: none; }
.about-section a:hover { text-decoration: underline; }
/* Sits in the band between the terminal and the about copy, styled as the
   quiet lead-in to that copy rather than as a fifth panel of the app. No
   card, no border, no colour of its own — this is prose for a reader who
   scrolled past the chart, and a crawler that never sees the chart at all. */
.item-seo { max-width: 900px; margin: 0 auto; padding: 18px 20px 0; }
.item-seo .is-sum { font-size: 13.5px; line-height: 1.62; color: var(--text-main); margin: 0; }
.item-seo .is-sum b { color: #fff; font-weight: 700; }
/* Two questions people actually type. Dim, small, and one line each — they
   earn their place on relevance, not attention. */
.item-seo .is-qa { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
.item-seo .is-qa p { font-size: 12.5px; line-height: 1.55; color: var(--text-muted); margin: 0; }
.item-seo .is-qa b { color: var(--text-main); font-weight: 700; }
/* Real anchors, not JS handlers: the point of this row is that a crawler can
   follow it. Nothing on the site linked one item page to another before, so
   the long tail was reachable only through the sitemap. */
.item-seo .is-rel { margin-top: 10px; font-size: 12.5px; color: var(--text-muted); }
.item-seo .is-rel a { color: var(--border-active); text-decoration: none; }
.item-seo .is-rel a:hover { text-decoration: underline; color: #fff; }
.item-seo .is-rel .is-sep { color: rgba(217,211,199,0.34); margin: 0 6px; }
@media (max-width: 640px) { .item-seo { padding: 14px 14px 0; } }
.about-section .about-intro { font-size: 14px; color: var(--text-main); padding-bottom: 12px; border-bottom: 1px solid var(--border-main); margin-bottom: 20px; }
/* A human aside, not more SEO copy — quieter and slightly set apart so it
   reads as a personal note rather than continuing the product description
   above it. */
.about-section .about-intro-2 { font-size: 13.5px; color: var(--text-main); margin-bottom: 20px; }
.about-section .about-questions { font-size: 13px; margin-bottom: 6px; color: var(--text-main); }
.about-section .about-qlist { list-style: none; padding: 0; margin: 0 0 20px; display: grid; gap: 6px; }
.about-section .about-qlist li { padding-left: 14px; position: relative; }
.about-section .about-qlist li::before { content: "→"; position: absolute; left: 0; color: var(--border-active); }
.about-section .about-qlist b { color: var(--text-main); }
/* Live component figures rather than screenshots. These reuse the app's own
   classes (.wl-item, .price-box, .flip-card …) exactly as the landing demo
   does, so they restyle with the product instead of going stale the moment
   the UI moves — which, at the rate this thing changes, a PNG would. They
   also cost no image bytes, stay sharp at any zoom, and are real text for a
   screen reader. Purely illustrative: nothing here is interactive. */
.about-section .ex { margin: 14px 0 22px; }
.about-section .ex-frame {
  border: 1px solid var(--border-main); border-radius: 10px; overflow: hidden;
  background: var(--bg-panel); max-width: 460px; pointer-events: none; user-select: none;
}
.about-section .ex-frame-pad { padding: 10px; }
.about-section .ex figcaption { font-size: 11.5px; color: var(--text-muted); margin-top: 7px; max-width: 460px; }
/* The app sizes these against its own containers; inside a prose column they
   need their own footing. */
.about-section .ex .price-strip { display: flex; gap: 6px; padding: 8px; }
.about-section .ex .price-box { flex: 1 1 0; min-width: 0; }
.about-section .ex .wl-item { cursor: default; }
.about-section .ex .flip-card { margin: 0; }
/* ── The workflow walkthrough ────────────────────────────────────────────
   Numbered steps rather than more prose, because the point of the section
   is that these four things happen in an ORDER and most people arriving
   have no idea what that order is. */
.about-section .flow { list-style: none; counter-reset: flowstep; padding: 0; margin: 18px 0 26px; }
.about-section .flow > li { counter-increment: flowstep; position: relative; padding: 0 0 22px 42px; }
.about-section .flow > li::before {
  content: counter(flowstep); position: absolute; left: 0; top: 0;
  width: 26px; height: 26px; border-radius: 50%;
  background: rgba(201,166,77,0.13); border: 1px solid rgba(201,166,77,0.45);
  color: var(--border-active); font-weight: 800; font-size: 12px;
  display: flex; align-items: center; justify-content: center;
}
/* The spine joining the steps — makes it read as one path rather than four
   unrelated tips. Stops at the last one. */
.about-section .flow > li::after {
  content: ""; position: absolute; left: 13px; top: 30px; bottom: 4px; width: 1px;
  background: linear-gradient(180deg, rgba(201,166,77,0.35), rgba(201,166,77,0.05));
}
.about-section .flow > li:last-child { padding-bottom: 0; }
.about-section .flow > li:last-child::after { display: none; }
.about-section .flow h3 { margin: 2px 0 6px; font-size: 14.5px; color: var(--text-main); font-weight: 700; }
.about-section .flow p { margin: 0 0 10px; }
.about-section .flow .ex { margin: 12px 0 4px; }

/* Drawn figures. Schematic on purpose: a screenshot of a chart states prices
   that will be wrong within the hour and stale UI within the month, while
   the thing being taught — the SHAPE of a setup — is the part that does not
   change. Inline SVG also themes with the page and costs no extra request. */
/* Capped to the same 460px the component figures use. Without a cap an SVG
   just takes the whole prose column, and at 840px wide the volume-profile
   diagram was bigger than the chart it describes. */
.about-section .fig-svg { display: block; width: 100%; max-width: 460px; height: auto; border-radius: 10px; border: 1px solid var(--border-main); background: var(--bg-panel); }
/* The three buy / sell / skip setups sit side by side where there is room
   and stack on a phone — they are meant to be COMPARED, which needs them in
   one eyeful. */
.about-section .scenarios { display: grid; gap: 16px; margin: 14px 0 22px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.about-section .scenarios figure { margin: 0; }
.about-section .scenarios figcaption { font-size: 11.5px; color: var(--text-muted); margin-top: 7px; }
.about-section .scenarios .verdict { display: block; font-weight: 800; font-size: 11px; letter-spacing: 0.5px; text-transform: uppercase; margin-bottom: 2px; }
.about-section .verdict.v-buy { color: var(--buy-color); }
.about-section .verdict.v-sell { color: var(--sell-color); }
.about-section .verdict.v-skip { color: var(--text-muted); }
/* ── Screenshots ─────────────────────────────────────────────────────────
   Portrait and landscape are genuinely different layouts, so they are
   genuinely different shapes on the page: a narrow column beside a wide one.
   The PNGs already carry their own handset bezel and rounded corners, with
   real transparency outside them — so there is deliberately no drawn shell
   here. A CSS frame around an image that already has one reads as a phone
   inside a phone. For the same reason the shadow is a drop-shadow FILTER
   rather than a box-shadow: the filter follows the alpha silhouette, so it
   hugs the rounded corners instead of casting a rectangle behind them. */
.about-section .shots { display: grid; gap: 22px; margin: 16px 0 22px; align-items: start; }
@media (min-width: 680px) {
  .about-section .shots { grid-template-columns: minmax(0, 210px) minmax(0, 1fr); gap: 26px; }
}
.about-section .shot { margin: 0; }
.about-section .shot figcaption { font-size: 11.5px; color: var(--text-muted); margin-top: 9px; }
.about-section .device { position: relative; }
.about-section .device img {
  display: block; width: 100%; height: auto;
  filter: drop-shadow(0 12px 26px rgba(0,0,0,0.55));
}
.about-section .device-portrait { max-width: 210px; }
.about-section .device-landscape { max-width: 500px; }
.about-section .about-devnote { font-size: 12.5px; color: var(--text-muted); padding-left: 12px; border-left: 2px solid var(--border-active); margin-bottom: 24px; }
.about-section .faq { margin-top: 4px; }
.about-section .faq details { border: 1px solid var(--border-main); border-radius: 6px; margin-bottom: 8px; background: var(--bg-panel); overflow: hidden; }
.about-section .faq summary { cursor: pointer; padding: 11px 14px; font-size: 13px; font-weight: 600; color: var(--text-main); list-style: none; display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.about-section .faq summary::-webkit-details-marker { display: none; }
.about-section .faq summary::after { content: "+"; color: var(--text-muted); font-size: 18px; font-weight: 400; flex-shrink: 0; }
.about-section .faq details[open] summary::after { content: "−"; }
.about-section .faq details[open] summary { border-bottom: 1px solid var(--border-main); }
.about-section .faq p { margin: 0; padding: 11px 14px; font-size: 12.5px; }
.about-section .popular { margin-top: 24px; }
.about-section .popular h3 { color: var(--text-main); font-size: 13px; font-weight: 700; margin: 14px 0 6px; text-transform: uppercase; letter-spacing: 0.5px; }
.about-section .popular .item-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 4px 14px; }
.about-section .popular .item-grid a { display: block; padding: 3px 0; font-size: 12.5px; color: var(--border-active); text-decoration: none; }
.about-section .popular .item-grid a:hover { text-decoration: underline; color: #fff; }
.wrap { max-width: 1800px; margin: 0 auto; padding: 8px; height: 100dvh; display: flex; flex-direction: column; gap: 8px; }
.content-area { display: flex; flex-direction: row; flex: 1; gap: 8px; min-height: 0; min-width: 0; }
.main-column { flex: 1; display: flex; flex-direction: column; min-width: 0; gap: 8px; }
/* Second query container, tracking the same width as .ticker-header inside it.
   Needed because a @container rule can style a container's DESCENDANTS but
   never the container element itself — so `@container tickerhdr` could set the
   toolbar's flex (a child) while `flex-wrap` on .ticker-header was silently
   dropped, which is a failure with no error and no warning. Querying the
   wrapper puts .ticker-header back in scope. */
.top-row-wrapper { display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; container-type: inline-size; container-name: tickerrow; }
/* No card chrome — the search input carries its own border, and that's
   the one control in this row that actually needs a boundary. Everything
   else reads fine as controls floating on the page background. */
.top-panel { padding: 4px 2px; display: flex; justify-content: flex-start; align-items: center; flex-wrap: wrap; gap: 8px; }
.panel-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
#logoBtn { font-weight: 800; color: #fff; cursor: pointer; margin-right: 6px; font-size: 16px; letter-spacing: -0.5px; user-select: none; transition: opacity 0.15s; display: flex; align-items: center; }
#logoBtn img { width: 18px; height: 18px; image-rendering: pixelated; margin-right: 4px; }
#logoBtn:hover { opacity: 0.8; }
.search-group { display: flex; align-items: center; gap: 4px; flex: 1 1 auto; min-width: 0; }
/* Search fills whatever room the row has (up to a sane cap so it doesn't
   sprawl on an ultrawide monitor) instead of sitting at a fixed width with
   dead space beside it — the search box is the single most-used control on
   the page, so it should get first claim on the row. Gold-tinted border so
   it's findable at a glance instead of blending into the toolbar chrome. */
.sbar { position: relative; flex: 1 1 220px; min-width: 0; max-width: 640px; }
.sbar input { width: 100%; background: var(--bg-input); border: 1px solid rgba(201,166,77,0.45); color: var(--text-main); border-radius: 4px; padding: 5px 8px 5px 28px; font-size: 16px; outline: none; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
.sbar input:hover { border-color: rgba(201,166,77,0.7); }
.sbar input:focus { border-color: var(--border-active); box-shadow: 0 0 0 2px rgba(201,166,77,0.18); }
/* Desktop: keep the whole top panel on ONE row when a maxed sidebar narrows
   the column. Placed after the base .panel-left / .sbar rules so these
   overrides win on source order. */
@media (min-width: 1024px) {
  .top-panel { flex-wrap: nowrap; }
  .panel-left { min-width: 0; flex-wrap: nowrap; }
  .search-group { min-width: 0; }
  .sbar { min-width: 0; }   /* keeps its clamp width, but may shrink when tight */
  /* Menu leads the row, left of the logo — same place YouTube and
     TradingView put theirs, and consistent with mobile now that the button
     is a real DOM sibling of the logo rather than nested in .search-group.
     Its bare-glyph + hover-fill treatment lives in the base rules, so
     there's nothing breakpoint-specific left to say about it here. */
  .search-group { flex: 1 1 auto; }
}
#itemIcon { position: absolute; left: 6px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; image-rendering: pixelated; }
/* Combined search+item+wiki overlay — off by default, enabled in landscape
   (see the landscape media query) where the standalone search is too narrow
   and the ticker name would otherwise duplicate the item. */
.sbar-item, .sbar-wiki { display: none; }
/* Primary "Save" affordance — lives inside the ticker header next to the
   item name, so it reads as "save THIS item". Bigger than the old top-panel
   pill, with a filled gold star + "Saved" label once active so state reads
   at a glance. The .fav-label / .fav-label-active spans swap via .active. */
/* Quiet by default, exactly like .qf-refresh beside it — transparent fill,
   hairline border, muted text. Gold is a STATE here, not a decoration: an
   unsaved item has nothing to report, so the button has no business being
   the brightest thing in the header. It was gold at rest, which spent the
   colour before it meant anything and left the saved state with nowhere
   louder to go.
   #btnShare wears this class too and never gets .active, so it is
   permanently in the quiet look — which is the right weight for a control
   that is always available and never a status. */
.fav-btn {
  background: transparent; border: 1px solid var(--border-main);
  border-radius: 6px; padding: 7px 14px 7px 11px; cursor: pointer;
  color: var(--text-muted);
  display: inline-flex; align-items: center; gap: 7px;
  font-family: inherit; font-weight: 800; font-size: 13px; letter-spacing: 0.35px;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.12s ease;
  flex-shrink: 0;
}
.fav-btn:hover { color: #fff; border-color: var(--border-active); }
.fav-btn:active { transform: translateY(1px); }
/* Saved is a STATE, not an action. A high-brightness glow and a thick lit
   border belong to primary actions — the thing you are being urged to press
   — and once this button has been pressed it is a passive indicator that
   should sit quietly while you read the chart.
   The saved state used to stack four escalations on top of the unsaved one:
   a 3.5x stronger gradient background, the border going to full gold, paler
   text, AND two shadows (a hard 3px ring plus a 20px bloom). Every one of
   them shouted, and none of them was the actual signal.
   It now keeps the same card and changes only the two things that mean
   "on": the star fills, and the label reads Saved. The border firms up a
   step because that is a state change you can see without it being a light
   source. */
.fav-btn.active {
  color: var(--fav-gold); border-color: rgba(255, 179, 0, 0.72);
  background: rgba(255, 179, 0, 0.10);
}
.fav-btn.active:hover { color: #FFD98A; border-color: var(--fav-gold); }
.fav-btn .fav-star { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
/* Outline, not filled. The gold already says "saved" — it is on the star,
   the label, the border and the fill tint — so filling the glyph as well
   was a fourth statement of the same fact, and a solid star is a heavier
   mark than anything else in that header. The outline keeps the button's
   silhouette identical between states, which is what makes the colour
   change read as a state rather than a different control. */
.fav-btn.active .fav-star { fill: none; }
.fav-btn .fav-label-active { display: none; }
.fav-btn.active .fav-label { display: none; }
.fav-btn.active .fav-label-active { display: inline; }
/* Button text labels only appear when the row truly has room. At
   1024–1279px the sidebar squeezes the main column enough that labeled
   buttons wrap the top panel onto 2–3 rows (with "Live" stranded on its
   own line) — icon-only buttons keep it to a single row in that band. */
.icon-btn { background: var(--bg-input); border: 1px solid var(--border-light); border-radius: 4px; padding: 4px 7px; cursor: pointer; color: var(--text-muted); font-size: 14px; line-height: 1; transition: all 0.15s ease; display: inline-flex; align-items: center; gap: 5px; }
.icon-btn .icon-label { display: none; font-size: 11px; font-weight: 700; letter-spacing: 0.3px; }
@media (min-width: 1280px) { .icon-btn .icon-label { display: inline; } }
.icon-btn:hover { color: var(--text-main); border-color: var(--border-active); }
.icon-btn.active { background: rgba(201, 166, 77, 0.16); border-color: var(--border-active); color: #F4E3B0; }
/* Overflow menu — Alerts/Bank/Help collapse into a ⋯ dropdown at every
   width (not just mobile) so the header stays to one clean row and the
   search bar — the one control people actually reach for on every visit —
   gets the room instead of splitting it three ways with icon buttons used
   far less often. */
.more-wrap { display: inline-flex; position: relative; flex: 0 0 auto; }
/* Bare glyph, no chip — a secondary control shouldn't carry the same
   boxed weight as the search field it sits beside. Hover/open still fill
   so losing the border doesn't cost the "this is clickable" affordance.
   The 3 lines are an SVG rather than the ☰ character on purpose: text
   glyphs carry font-specific baseline offsets and won't reliably centre. */
.more-toggle { display: inline-flex; background: transparent; border-color: transparent; padding: 4px 8px; min-width: 34px; min-height: 30px; align-items: center; justify-content: center; }
.more-toggle:hover, .more-toggle[aria-expanded="true"] { background: var(--bg-input); border-color: var(--border-light); color: var(--text-main); }
/* Slide-in nav drawer rather than a small dropdown. A dropdown anchored to
   a far-left button would open off-screen, and the menu is worth more than
   four icons in a popover — this gives room to actually navigate the site.
   Kept in flow and moved with transform (not display:none) so it animates;
   visibility keeps it out of the tab order and away from pointer events
   while closed, which offscreen-but-visible content would not. */
.more-actions {
  display: flex; position: fixed; top: 0; bottom: 0; left: 0; z-index: 1100;
  width: min(290px, 84vw); flex-direction: column; gap: 2px; padding: 10px;
  background: var(--bg-panel); border-right: 1px solid var(--border-light);
  box-shadow: 8px 0 34px rgba(0,0,0,0.6);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  transform: translateX(-100%); visibility: hidden;
  transition: transform 0.22s ease, visibility 0.22s;
}
.more-actions.open { transform: translateX(0); visibility: visible; }
.drawer-backdrop {
  position: fixed; inset: 0; z-index: 1090; background: rgba(5,7,12,0.6);
  backdrop-filter: blur(2px); opacity: 0; visibility: hidden;
  transition: opacity 0.22s ease, visibility 0.22s;
}
.drawer-backdrop.open { opacity: 1; visibility: visible; }
/* Close control is a second hamburger sitting where the one that opened the
   drawer was, not an × in the far corner — the same button in the same place
   toggling both ways is how every major nav drawer behaves, and it means the
   pointer never has to travel across the panel to undo the tap it just made.
   Padding here mirrors .more-toggle's so the two land on the same spot. */
.drawer-head { display: flex; align-items: center; gap: 4px; padding: 0 0 10px; }
.drawer-brand { display: flex; align-items: center; gap: 8px; font-weight: 800; color: var(--text-main); font-size: 15px; }
.drawer-brand img { width: 20px; height: 20px; image-rendering: pixelated; }
.drawer-close { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; min-width: 34px; min-height: 30px; background: transparent; border: none; color: var(--text-main); cursor: pointer; padding: 4px 8px; border-radius: 6px; }
.drawer-close .ui-ic { width: 18px; height: 18px; vertical-align: 0; }
.drawer-close:hover { color: #fff; background: var(--bg-input); }
.drawer-sep { height: 1px; background: var(--border-main); margin: 8px 4px; flex: 0 0 auto; }
.drawer-label { color: var(--text-muted); font-size: 10px; font-weight: 800; letter-spacing: 0.7px; text-transform: uppercase; padding: 4px 10px; }
.drawer-link { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 6px; color: var(--text-main); text-decoration: none; font-size: 13.5px; }
.drawer-link:hover { background: var(--bg-input); text-decoration: none; }
.drawer-link.drawer-rsn b { color: var(--fav-gold); font-weight: 800; }
/* content-visibility on .about-section means the anchor target may still be
   skipped at jump time; scroll-margin keeps it clear of nothing in
   particular, but the highlight is what proves the jump landed. */
#meet-the-dev { scroll-margin-top: 24px; outline: none; transition: background 0.4s ease, border-left-color 0.4s ease; }
#meet-the-dev.jump-flash { background: rgba(201,166,77,0.10); border-left-color: var(--fav-gold); }
.more-actions .icon-btn { width: 100%; justify-content: flex-start; gap: 10px; padding: 9px 10px; font-size: 14px; border-color: transparent; background: transparent; }
.more-actions .icon-btn:hover, .more-actions .icon-btn:active { background: var(--bg-input); }
.more-actions .icon-label { display: inline; font-size: 13px; }
.modal-overlay { position: fixed; inset: 0; background: rgba(5, 7, 12, 0.75); backdrop-filter: blur(3px); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px; overscroll-behavior: contain; }
.modal-card { background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px; width: 100%; max-width: 640px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 12px 40px var(--shadow); overflow: hidden; }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border-main); flex-shrink: 0; }
.modal-header h3 { margin: 0; font-size: 16px; color: #fff; font-weight: 800; }
.modal-close { background: transparent; border: none; color: var(--text-muted); font-size: 24px; cursor: pointer; line-height: 1; padding: 0 4px; }
.modal-close:hover { color: #fff; }
.modal-body { padding: 14px 16px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.glossary-section { margin-top: 14px; margin-bottom: 8px; font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; padding-bottom: 4px; border-bottom: 1px solid var(--border-main); }
.glossary-section:first-of-type { margin-top: 0; }
.glossary-row { display: grid; grid-template-columns: 110px 1fr; gap: 12px; padding: 8px 4px; font-size: 12px; color: var(--text-main); align-items: center; line-height: 1.4; }
.glossary-row > :first-child { justify-self: start; }
/* RuneLite bridge section inside the Bank modal. */
.bk-rl { margin: 10px 0 12px; }
.bk-rl-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.rl-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); margin-right: 7px; vertical-align: 1px; }
.rl-dot.on { background: var(--rs-green-deep); box-shadow: 0 0 6px rgba(16,185,129,0.7); }
.rl-body { margin-top: 8px; font-size: 12px; }
.rl-hint { color: var(--text-muted); line-height: 1.5; }
.rl-hint a { color: var(--border-active); }
.rl-profit { font-weight: 800; font-size: 14px; margin-bottom: 6px; }
.rl-flip { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding: 5px 6px; border-bottom: 1px solid rgba(60,53,43,0.4); cursor: pointer; border-radius: 3px; }
.rl-flip:hover { background: var(--bg-input); }
.rl-flip .rl-name { color: var(--text-main); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rl-flip .rl-nums { flex-shrink: 0; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.rl-flip .pos { color: var(--rs-green-deep); font-weight: 700; }
.rl-flip .neg { color: var(--negative); font-weight: 700; }
/* Small "Linked" pill on the sidebar's Favorites title when the RuneLite
   bridge is actively merging in plugin favorites — reuses .rl-dot so the
   same green pulse means the same thing in both places. */
.rl-linked-badge { display: inline-flex; align-items: center; margin-left: 6px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; color: var(--rs-green-deep); }
.rl-linked-badge .rl-dot { margin-right: 4px; }
/* Bank of Gielinor — shared form / stats / row scaffolding (the `bk-` prefix
   is used across this whole feature; CSS reused by the modal header etc.). */
.bk-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 14px; }
.bk-stat { background: var(--bg-input); border: 1px solid var(--border-main); border-radius: 4px; padding: 8px 10px; text-align: center; }
.bk-stat.headline { grid-column: 1 / -1; padding: 14px 12px; background: linear-gradient(135deg, rgba(255,179,0,0.18), rgba(255,210,77,0.08)); border-color: rgba(255,179,0,0.5); }
.bk-stat-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
.bk-stat-val { font-size: 18px; font-weight: 900; margin-top: 2px; letter-spacing: -0.5px; }
.bk-stat.headline .bk-stat-val { font-size: 28px; color: #FFD24D; letter-spacing: -1px; }
.bk-totalpl-pos { color: var(--positive); }
.bk-totalpl-neg { color: var(--negative); }
.bk-item-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: var(--bg-input); border: 1px solid var(--border-main); border-radius: 4px; margin-bottom: 8px; }
.bk-item-icon { width: 18px; height: 18px; image-rendering: pixelated; flex-shrink: 0; }
.bk-item-icon:not([src]), .bk-item-icon[src=""] { display: none; }
.bk-item-name { font-size: 13px; font-weight: 600; color: var(--text-main); }
.bk-form input { background: var(--bg-input); border: 1px solid var(--border-light); color: var(--text-main); border-radius: 4px; padding: 6px 8px; font-size: 13px; outline: none; font-family: inherit; min-width: 0; }
.bk-form input:focus { border-color: var(--border-active); }
.bk-form-grid { display: grid; grid-template-columns: 1fr 1fr 0.7fr auto; gap: 6px; align-items: stretch; }
.bk-submit { background: var(--border-active); border: none; color: var(--accent-ink); border-radius: 4px; padding: 6px 12px; font-weight: 700; font-size: 12px; cursor: pointer; }
.bk-submit:hover { background: var(--accent-bright); }
.bk-hint { font-size: 10px; color: var(--text-muted); margin-top: 4px; min-height: 14px; }
.bk-hint.error { color: var(--negative); }
.bk-section-title { margin-top: 16px; margin-bottom: 6px; font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }

/* Notification Center modal */
.nc-section { margin-bottom: 18px; }
.nc-section-title { font-size: 13px; font-weight: 800; color: #fff; margin: 0 0 4px; }
.nc-section-desc { font-size: 11px; color: var(--text-muted); line-height: 1.4; margin: 0 0 8px; }
.nc-master { background: var(--bg-input); border: 1px solid var(--border-main); border-radius: 6px; padding: 12px 14px; }
.nc-master-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.nc-master-label { font-size: 13px; font-weight: 700; color: var(--text-main); }
.nc-master-state { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.nc-master-state.granted { color: var(--positive); }
.nc-master-state.denied { color: var(--negative); }
.nc-test-btn { margin-top: 10px; background: transparent; border: 1px solid var(--border-light); color: var(--text-main); padding: 6px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
.nc-test-btn:hover:not(:disabled) { border-color: var(--border-active); color: #fff; }
.nc-test-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.nc-toggle { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
.nc-toggle input { opacity: 0; width: 0; height: 0; }
.nc-toggle-slider { position: absolute; cursor: pointer; inset: 0; background: var(--bg-input); border: 1px solid var(--border-main); transition: 0.2s; border-radius: 24px; }
.nc-toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; top: 2px; background: var(--text-muted); transition: 0.2s; border-radius: 50%; }
.nc-toggle input:checked + .nc-toggle-slider { background: rgba(0, 200, 100, 0.25); border-color: var(--positive); }
.nc-toggle input:checked + .nc-toggle-slider:before { transform: translateX(20px); background: var(--positive); }
.nc-list { border: 1px solid var(--border-main); border-radius: 4px; overflow: hidden; }
.nc-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-bottom: 1px solid var(--border-main); background: var(--bg-input); font-size: 12px; }
.nc-row:last-child { border-bottom: none; }
.nc-row img { width: 22px; height: 22px; image-rendering: pixelated; flex-shrink: 0; }
.nc-row-name { font-weight: 700; color: var(--text-main); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nc-row-caption { font-size: 10px; color: var(--text-muted); margin-top: 1px; }
.nc-row-name-stack { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.nc-pct-input { width: 56px; background: var(--bg-input); border: 1px solid var(--border-light); color: var(--text-main); border-radius: 3px; padding: 4px 6px; font-size: 12px; outline: none; text-align: right; }
.nc-pct-input:focus { border-color: var(--border-active); }
.nc-pct-suffix { font-size: 11px; color: var(--text-muted); }
.nc-empty { padding: 12px; font-size: 12px; color: var(--text-muted); text-align: center; background: var(--bg-input); border: 1px dashed var(--border-main); border-radius: 4px; }
.bk-holdings { border: 1px solid var(--border-main); border-radius: 4px; overflow: hidden; }
.bk-row { display: grid; grid-template-columns: 24px 1fr auto auto auto; gap: 8px; padding: 8px 10px; font-size: 12px; border-bottom: 1px solid rgba(60,53,43,0.5); align-items: center; cursor: pointer; }
.bk-row:hover { background: var(--bg-input); }
.bk-row:last-child { border-bottom: none; }
.bk-row img { width: 20px; height: 20px; image-rendering: pixelated; }
.bk-row .bk-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bk-row .bk-sub { font-size: 9px; color: var(--text-muted); }
.bk-row .bk-val { text-align: right; font-weight: 800; }
.bk-row .bk-pl { text-align: right; font-weight: 800; min-width: 64px; font-size: 11px; }
.bk-row .bk-pl.pos { color: var(--positive); }
.bk-row .bk-pl.neg { color: var(--negative); }
.bk-row .bk-del { background: transparent; border: none; color: var(--text-muted); cursor: pointer; padding: 0 4px; font-size: 14px; }
.bk-row .bk-del:hover { color: var(--negative); }
.bk-row.empty { display: block; text-align: center; color: var(--text-muted); padding: 12px; cursor: default; }
.bk-row.empty:hover { background: transparent; }
.bk-icon { display: inline-block; vertical-align: middle; }
@media screen and (max-width: 640px) {
  .bk-form-grid { grid-template-columns: 1fr 1fr; }
  .bk-form-grid .bk-submit, .bk-form-grid button[type="submit"] { grid-column: 1 / -1; padding: 8px; }
  .bk-row { grid-template-columns: 22px 1fr auto auto; }
  .bk-row .bk-val { display: none; }
}
.suggest { margin-top: 4px; border: 1px solid var(--border-light); border-radius: 4px; max-height: 200px; overflow-y: auto; background: var(--bg-panel); position: absolute; z-index: 100; width: 260px; box-shadow: 0 8px 16px var(--shadow); }
.suggest div { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border-main); cursor: pointer; font-size: 13px; }
.suggest div:last-child { border-bottom: none; }
.suggest div:hover { background: var(--bg-input); color: #fff; }
.suggest img { width: 16px; height: 16px; image-rendering: pixelated; }
.suggest .suggest-header { padding: 4px 10px; font-size: 9px; font-weight: 700; letter-spacing: 0.5px; color: var(--text-muted); text-transform: uppercase; background: rgba(0,0,0,0.25); cursor: default; }
.suggest .suggest-header:hover { background: rgba(0,0,0,0.25); color: var(--text-muted); }
/* Compact mode pill (mobile top panel — desktop hides this and uses the
   sidebar version). Both sides share ONE active treatment, gold — see the
   note on the shared .active rule below. */
/* No shell around the pair — the active side's tinted text plus its 2px
   underline already reads as a tab group, and the box was competing with
   the search field beside it. The underline is what keeps this from
   looking like two unrelated labels, so it stays load-bearing here. */
.mode-pill { display: flex; background: transparent; border: none; border-radius: 6px; padding: 2px; height: 30px; flex-shrink: 0; gap: 2px; }
.mode-pill button { background: transparent; border: none; color: var(--text-muted); padding: 0 8px; cursor: pointer; font-weight: 800; font-size: 11px; border-radius: 4px; height: 100%; letter-spacing: 0.4px; text-transform: uppercase; transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; }
.mode-pill button:active { transform: translateY(1px); }
/* One active treatment for BOTH sides, gold. F2P used to light up green,
   which collides with what green already means on this page: it is the
   profit/positive colour and the 5D-high badge, so a green chip
   sitting in the header read for a beat like price action rather than a
   filter. Gold is the app's existing "you selected this" colour — the
   favourites star, active borders, the timeframe pills — so using it for
   both keeps the header to one idea: gold means chosen. Which side is which
   is carried by the F2P / P2P labels and the underline, not by hue, exactly
   as a two-tab segmented control normally works.
   Shared by the compact and sidebar pills so the two can't drift apart. */
.mode-pill .mp-f2p.active, .mode-pill .mp-p2p.active,
.mode-pill-side .mp-f2p.active, .mode-pill-side .mp-p2p.active {
  background: linear-gradient(180deg, rgba(255,179,0,0.16) 0%, rgba(255,179,0,0.06) 100%);
  color: #FFD97A;
  box-shadow: inset 0 -2px 0 var(--fav-gold);
}

/* Sidebar item-list filter — the primary control on desktop. Hidden by
   default; the desktop media query below shows it (and hides the small
   top-panel pill). Sleek segmented control: one pill shell, split by the
   two buttons; the active side gets a subtle gold fill (shared rule above)
   so state reads at a glance without shouting. */
.mode-pill-side { display: none; }
.mode-pill-side .mps-btn {
  flex: 1 1 0; min-width: 0;
  padding: 8px 10px;
  background: transparent; border: none;
  border-radius: 6px;
  color: var(--text-muted); cursor: pointer; font-family: inherit;
  font-size: 12.5px; font-weight: 800; letter-spacing: 0.6px; text-transform: uppercase;
  transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.mode-pill-side .mps-btn:hover { color: var(--text-main); }
/* Keep this row lean — every px of chrome here is a px the chart loses.
   nowrap, deliberately. This row used to wrap, on the theory that a long
   item name plus the toolbar might not fit and dropping the timeframes to
   their own line beat clipping them. In practice the wrap wasn't a
   last-resort at all: the container query below set the toolbar to
   flex-basis 100%, which FORCES a second line whenever the header is
   narrow, whether or not the content would have fitted. Measured, the
   toolbar rendered 653px wide at a 661px header and 266px at a 738px one —
   same six buttons. So the "unavoidable" wrap was self-inflicted.
   Now the row stays one row and the overflow story is explicit: the item
   name truncates first (it has the most redundant width — it's also in the
   search box and the tab title), and only if that isn't enough does the
   toolbar scroll horizontally. A wrapped row costs ~48px of vertical space
   that comes straight out of the chart. */
/* No card here either (see .top-panel) — the item name/price is its own
   visual anchor and the timeframe pills carry their own selected-state
   highlight, so a wrapping box was just doubling up on both. */
/* Packed left, never space-between. space-between sets the Save button
   adrift: it's spaced against whatever else shares its flex line, so its
   distance from the item name is a function of viewport width — and once
   the header wraps and the timeframe toolbar drops to its own line, Save is
   the only thing left to push, so it flies to the far right edge. Measured
   before this change, the name-to-Save gap ran 372px at 1023w, 249px at
   900w and 49px at 700w, versus a flat 12px above 1024. Packing left makes
   it the same 12px everywhere, so Save always reads as belonging to the
   item name it saves — TradingView-style symbol+star. */
/* Asymmetric padding on purpose. Above the header there is only the search
   row and its own 4px; below it are the Target boxes, and the .main-column
   flex gap already puts 8px there. 6px of padding-bottom on top of that gap
   measured 16px of empty band between the timeframe pills and the boxes —
   the widest unexplained space on the page. 2px leaves 10px total, which
   still separates them without reading as a hole. */
.ticker-header { padding: 6px 4px 2px; display: flex; justify-content: flex-start; align-items: center; flex-wrap: nowrap; gap: 4px 12px; flex-shrink: 0; container-type: inline-size; container-name: tickerhdr; }
/* Shrink factor 8 vs the toolbar's 1: both CAN shrink, but the name gives up
   width roughly eight times faster, so it ellipsizes long before the pills
   start scrolling. The name is the cheapest thing on this row to lose — it's
   also in the search box, the tab title and the chart's own header.
   The floor is min-width:AUTO, not a pixel number. A flex item with
   min-width:0 collapses to zero no matter what its children declare, and the
   children then paint straight over the next item — but a fixed floor is just
   as wrong, because it can't know how wide the content is: 120px held a 650gp
   price fine and let a 59,930,778 one spill 21px over the Save button.
   auto resolves to the content-based minimum, which is exactly "as narrow as
   this can get without overlapping". The name still ellipsizes inside it: an
   element with overflow:hidden contributes zero to that minimum, so the floor
   ends up being the price row's width and nothing more. */
.ticker-title-group { display: flex; align-items: center; gap: 10px; min-width: auto; flex: 0 8 auto; }
/* min-width:0 is what lets a flex item shrink below its content size at all;
   without it the toolbar would refuse to shrink and push the row wide. The
   buttons themselves stay rigid (flex:0 0 auto) so shrinking scrolls the
   group rather than squashing six pills into unreadable slivers. */
.ticker-header .toolbar { flex: 0 1 auto; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; -ms-overflow-style: none; overscroll-behavior-x: contain; }
.ticker-header .toolbar::-webkit-scrollbar { display: none; }
.ticker-header .toolbar button { flex: 0 0 auto; }
/* A scroll strip with no scrollbar and no cut-off pill looks like a strip that
   simply ends — 5Y would just appear not to exist. Fade the edge that still
   has content behind it. Class-driven because CSS can't ask "am I overflowing";
   JS toggles these (see keepActiveViewVisible) on load, resize and scroll. */
.ticker-header .toolbar.tb-more-right { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent 100%); mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent 100%); }
.ticker-header .toolbar.tb-more-left { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 22px), transparent 100%); mask-image: linear-gradient(to left, #000 calc(100% - 22px), transparent 100%); }
.ticker-header .toolbar.tb-more-left.tb-more-right { -webkit-mask-image: linear-gradient(to right, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%); mask-image: linear-gradient(to right, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%); }
@media (min-width: 1024px) {
  /* TradingView's company page gives the logo real presence — a sizeable
     mark in its own rounded chip beside the name, not a small inline icon.
     Match that proportion: a bigger backdrop chip (item sprites are small,
     irregular pixel art, not square logos, so they get a panel behind them
     rather than just scaling the raw image) with the sprite centered inside,
     still pixelated so it stays crisp at the larger size. */
  #tickerHeaderIconLink {
    width: 48px; height: 48px; flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--bg-input);
    border: 1px solid var(--border-main);
    border-radius: 10px;
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 1px 4px rgba(0,0,0,0.35);
  }
  /* 34 -> 40 inside the same 48px plate. The plate is what makes a dark icon
     legible, but it only helps in proportion to how much of it the icon fills;
     34px in a 48px chip is mostly empty plate. 40 keeps a 4px margin so it
     still reads as a slot rather than a cropped image. */
  #tickerHeaderIconLink img { width: 40px; height: 40px; max-width: 40px; max-height: 40px; }
  .ticker-title-group { gap: 12px; }
}
.ticker-title-group img { width: 34px; height: 34px; image-rendering: pixelated; }
.ticker-wiki-link { display: inline-flex; color: inherit; text-decoration: none; line-height: 0; cursor: pointer; }
.ticker-name-link { line-height: normal; min-width: 0; max-width: 100%; }
/* auto, so the live price sets the floor — see .ticker-title-group. The h2
   above it has overflow:hidden and so still contributes zero, keeping its
   ellipsis. */
.ticker-info { min-width: auto; }
.ticker-name-link:hover h2 { text-decoration: underline; text-decoration-color: var(--text-muted); text-underline-offset: 3px; }
.ticker-wiki-link:focus-visible { outline: 2px solid var(--border-active); outline-offset: 2px; border-radius: 4px; }
.ticker-info h2 { margin: 0; font-size: 20px; font-weight: 800; color: #fff; letter-spacing: -0.4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* The item name has always linked to the OSRS wiki, but it rendered as plain
   white heading text, so nothing suggested it was clickable — the underline
   only showed once you were already hovering it. This glyph is the standing
   affordance: muted enough to stay out of the way of the name, and it picks
   up the gold alongside the underline on hover so the whole title reads as
   one control. Sits OUTSIDE the h2 on purpose — inside, the h2's
   text-overflow would clip the glyph away on exactly the long item names
   where a wiki link is most useful. Only the desktop header carries it:
   both mobile layouts hide .ticker-title-group entirely and surface the
   wiki through .sbar-wiki instead. */
.wiki-hint {
  flex: 0 0 auto; align-self: center; width: 13px; height: 13px;
  margin-left: 6px; color: var(--text-muted);
  transition: color .12s ease;
}
.ticker-name-link:hover .wiki-hint { color: var(--border-active); }
/* Hero live price + timeframe change, left-aligned under the item name —
   the familiar TradingView symbol-header treatment. Kept medium so it
   frames the live quote without competing with the Target Buy/Sell boxes
   (our actual hero). Desktop-only; mobile hides .ticker-title-group. */
.ticker-price-row { display: flex; align-items: baseline; gap: 8px; margin-top: 1px; }
.ticker-price-row .price { font-size: 22px; font-weight: 800; color: #fff; letter-spacing: -0.4px; line-height: 1; }
.ticker-price-row .price .tlp-gp { font-size: 11px; font-weight: 600; color: var(--text-muted); letter-spacing: 0; }
.ticker-price-row .change { font-size: 13px; font-weight: 700; }
/* Empty for every normal item now, and an empty flex child would still eat
   the row's 8px gap after the price. */
.ticker-price-row .change:empty { display: none; }
.change.positive { color: var(--positive); }
.change.negative { color: var(--negative); }
.change.neutral { color: var(--text-muted); }
/* No pill-strip background — the active button's own highlight (below)
   is enough to show selection without a second nested box. */
.toolbar { display: flex; padding: 2px 0; gap: 4px; }
.toolbar button { background: transparent; border: none; color: var(--text-muted); border-radius: 3px; padding: 4px 10px; cursor: pointer; font-weight: 600; font-size: 12px; transition: all 0.15s; display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 45px; }
/* Desktop time-range buttons: wide enough to stay a primary affordance, but
   short — vertical space here comes straight out of the chart's height. */
@media (min-width: 1024px) {
  .toolbar { padding: 4px 0; gap: 6px; }
  .toolbar button { padding: 4px 18px; font-size: 14px; min-width: 72px; font-weight: 700; letter-spacing: 0.5px; }
  .toolbar button .pct-chg { font-size: 10px; margin-top: 1px; }
  /* F2P/Members lives next to the search bar at every width — it reads as
     "what am I searching" and belongs beside the search box, not off in the
     sidebar. .mode-pill-side stays available (same markup/JS, shared
     .mp-f2p/.mp-p2p classes) but is never shown. */
}
.toolbar button:hover { color: var(--text-main); }
.toolbar button.active { background: var(--border-light); color: #fff; }
.pct-chg { font-size: 9px; font-weight: normal; margin-top: 2px; }
.pct-chg.positive { color: var(--positive); }
.pct-chg.negative { color: var(--negative); }
.pct-chg.neutral { color: var(--text-muted); }
/* No stroke — the chart is the hero, it gets contained by its own
   background contrast (--bg-chart vs. the page), not a drawn edge. */
.chart-container { flex: 1; display: flex; flex-direction: column; min-width: 0; background: var(--bg-chart); border-radius: 6px; position: relative; overflow: hidden; }
.chart { flex: 1; min-height: 0; width: 100%; display: block; touch-action: none; }
/* On a touch device the page MUST stay scrollable through the chart.
   touch-action:none hands every gesture to the canvas, which is right on
   desktop (the wheel still scrolls the page) and a trap on a phone: with the
   chart filling the screen there was no way to reach anything below it —
   reported as "can't scroll past the chart", and there is no visible cue that
   the canvas is why.
   pan-y gives vertical drags back to the browser. The cost is real and worth
   naming: one-finger vertical panning and the y-axis scale drag are no longer
   available on touch, because the browser consumes those gestures now.
   Horizontal panning, taps, and the double-tap resets all still work. Being
   unable to scroll the page is a dead end; losing a gesture is not. */
@media (pointer: coarse) {
  .chart { touch-action: pan-y; }
}
/* The price strip lives above the chart now (not as an overlay) so the
   target / grade boxes can never cover chart data. The flex-row puts buy +
   sell side by side; on narrow widths they wrap and the chart simply moves
   down a little. */
.price-strip { display: flex; gap: 8px; padding: 0; flex-shrink: 0; flex-wrap: wrap; }
.price-strip .price-box { flex: 1 1 0; min-width: 0; }
/* ── Activity bar ────────────────────────────────────────────────────────
   Indeterminate, not a progress bar: we genuinely do not know how far along a
   fetch is, and a bar that fills 0->100% would be inventing that. This is the
   Material two-segment motion — a long segment that accelerates across, then a
   short one chasing it — which reads as "working" rather than "23% done".
   The track keeps its 3px at all times and only the paint changes, so starting
   or finishing a load never shifts the chart underneath it.
   Placed on the seam between the target numbers and the chart because those
   are the two things the fetch is about to change; put anywhere else it would
   be decoration. */
.load-bar {
  position: absolute; top: 0; left: 0; right: 0; height: 3px; z-index: 6;
  pointer-events: none;              /* never steals a drag from the chart */
  overflow: hidden;
  background: transparent;
  transition: background 0.2s ease;
}
.load-bar span {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: 100%; transform-origin: left center;
  background: linear-gradient(90deg, rgba(229,184,66,0.35), var(--border-active) 45%, #FFD98A 70%, rgba(229,184,66,0.35));
  will-change: transform; transform: scaleX(0);
}
.load-bar.is-active { background: rgba(229,184,66,0.10); }
/* The two cubic-beziers and the 2.1s cycle are Material's — the shape of the
   easing is the whole effect, and eyeballing an approximation reads as a
   generic slider instead. */
.load-bar.is-active span:nth-child(1) { animation: lb-long 2.1s cubic-bezier(0.65,0.815,0.735,0.395) infinite; }
.load-bar.is-active span:nth-child(2) { animation: lb-short 2.1s cubic-bezier(0.165,0.84,0.44,1) 1.15s infinite; }
@keyframes lb-long {
  0%   { transform: translateX(-35%) scaleX(0.35); }
  60%  { transform: translateX(100%) scaleX(0.9); }
  100% { transform: translateX(100%) scaleX(0.9); }
}
@keyframes lb-short {
  0%   { transform: translateX(-100%) scaleX(0.12); }
  70%  { transform: translateX(110%) scaleX(0.6); }
  100% { transform: translateX(110%) scaleX(0.6); }
}
/* Motion is the entire point here, so with reduced-motion the honest
   substitute is a static presence, not a slower slide. */
@media (prefers-reduced-motion: reduce) {
  .load-bar.is-active span:nth-child(1) { animation: none; transform: scaleX(1); opacity: 0.5; }
  .load-bar.is-active span:nth-child(2) { animation: none; transform: scaleX(0); }
}
/* Target Buy / Target Sell are the hero of the app — these are the actual
   recommendation. Side-tinted background, a colored accent stripe on the
   left, and a soft glow on the number itself so the eye lands here first
   instead of having to scan past the chart and sidebar. Buy / Sell color
   identity (gold / teal) stays consistent with the rest of the app. The
   3-side border is deliberately faint (not "light") — .selected below is
   the one that should visibly pop, not both boxes equally. */
.price-box {
  position: relative;
  background: rgba(32, 28, 22, 0.85);
  border: 1px solid var(--border-hair);
  border-left-width: 3px;
  border-radius: 5px;
  padding: 7px 12px;
  min-width: 140px;
  pointer-events: auto;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}
.price-box.low  { border-left-color: var(--buy-color);  background: linear-gradient(90deg, rgba(255,184,0,0.10), rgba(32,28,22,0.85) 35%); }
.price-box.high { border-left-color: var(--sell-color); background: linear-gradient(90deg, rgba(38, 169, 171,0.12), rgba(32,28,22,0.85) 35%); }
/* Same white the favorites row uses for ITS selection — rgba(255,255,255,0.42)
   rather than solid #fff. Two controls that both mean "this is the one you
   picked" were shouting at different volumes, and the louder of the two sat
   directly above the chart where it competed with the data. The coloured left
   edge and the glow below still carry the buy/sell identity; only the white
   outline calms down. */
.price-box.selected { border-color: rgba(255,255,255,0.42); border-left-width: 3px; }
.price-box.low.selected  { border-left-color: var(--buy-color);  box-shadow: 0 0 0 1px rgba(255,184,0,0.25), 0 4px 12px rgba(255,184,0,0.10); }
.price-box.high.selected { border-left-color: var(--sell-color); box-shadow: 0 0 0 1px rgba(38, 169, 171,0.30), 0 4px 12px rgba(38, 169, 171,0.12); }
.price-label { font-size: 10px; color: var(--text-muted); font-weight: bold; text-transform: uppercase; display: flex; justify-content: space-between; align-items: center; gap: 8px; letter-spacing: 0.4px; }
.live-val { font-weight: normal; color: #E0D9CB; font-size: 10px; text-transform: none; letter-spacing: 0; }
.live-age { color: var(--text-muted); }
.price-box.low .price-val  { color: var(--buy-color);  text-shadow: 0 0 12px rgba(255,184,0,0.30); }
.price-box.high .price-val { color: var(--sell-color); text-shadow: 0 0 12px rgba(38, 169, 171,0.30); }
.price-val { font-size: 19px; font-weight: 900; margin-top: 2px; letter-spacing: -0.4px; display: flex; align-items: center; gap: 6px; }
/* Long-price fit — see pvFit(). em, so one rule covers all six breakpoint
   sizes of .price-val; min-width:0 lets the span actually shrink inside the
   flex row rather than forcing the overflow it is trying to avoid. */
.price-val .pv-num { min-width: 0; }
.price-val .pv-num.pv-md { font-size: 0.86em; letter-spacing: -0.3px; }
.price-val .pv-num.pv-sm { font-size: 0.70em; letter-spacing: -0.2px; }
.price-val .pv-num.pv-xs { font-size: 0.58em; letter-spacing: 0; }
/* Nudge the target price gp-by-gp without opening the Potential Profit
   breakdown. These used to be two 16x11 arrows stacked in the corner of a
   card with plenty of room to spare — a sniper shot for a 1 gp change, and
   the wrong side of Fitts's law. Now they flank the numeral as square
   buttons, sized up with the card at each breakpoint, tinted to their own
   side (gold buys, teal sells) so the pair reads as part of the box rather
   than generic browser chrome.
   Still hidden below 641px: a phone price box is 3px-padded and 17px-tall
   text, with no room for a target big enough to be worth tapping, and the
   calculator already offers a fine-grained input there. */
.price-step {
  display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 24px; height: 24px; padding: 0; border-radius: 6px;
  border: 1px solid transparent; background: rgba(255,255,255,0.06);
  font: 900 15px/1 inherit; color: var(--text-muted);
  cursor: pointer; user-select: none;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
.price-box.low  .price-step { color: rgba(229, 184, 66,0.75); }
.price-box.high .price-step { color: rgba(38, 169, 171,0.85); }
.price-box.low .price-step:hover {
  background: rgba(255,184,0,0.18); border-color: rgba(255,184,0,0.5);
  color: var(--buy-color); box-shadow: 0 0 10px rgba(255,184,0,0.22);
}
.price-box.high .price-step:hover {
  background: rgba(38, 169, 171,0.18); border-color: rgba(38, 169, 171,0.5);
  color: var(--sell-color); box-shadow: 0 0 10px rgba(38, 169, 171,0.22);
}
.price-step:active { transform: translateY(1px); }
.price-step:focus-visible { outline: 2px solid var(--border-active); outline-offset: 1px; }
@media (max-width: 640px) {
  .price-step { display: none; }
}
.chart-footer { background: var(--bg-panel); border-top: 1px solid var(--border-main); padding: 3px 10px; display: flex; gap: 10px; font-size: 9px; color: var(--text-muted); align-items: center; flex-shrink: 0; flex-wrap: wrap; }
/* Dropped on compact screens. It's a colour key you read once and then never
   again, but it costs 17px in landscape and 37px in portrait (where it wraps
   to two lines) — more than the volume pane did, out of the same budget the
   chart is short of. The information stays reachable two ways that don't cost
   permanent height: tapping any marker on the chart names it in the tooltip,
   and the glossary (Menu → Help) has the full key. Desktop keeps it. */
@media (max-width: 640px), (max-height: 600px) and (orientation: landscape) {
  .chart-footer.legend { display: none; }
}
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-dash { width: 10px; height: 2px; display: inline-block; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.legend-ring { width: 8px; height: 8px; border-radius: 50%; display: inline-block; border: 2px solid; box-sizing: border-box; }
.legend .low .legend-dash, .legend .low .legend-dot { background: var(--buy-color); }
.legend .high .legend-dash, .legend .high .legend-dot { background: var(--sell-color); }
.legend .rlow .legend-ring { border-color: var(--buy-color); }
.legend .rhigh .legend-ring { border-color: var(--sell-color); }
/* pg-* type styles are kept for the GLOSSARY rows and the Analyst gauge
   (.pg-help "?" chip); the per-box grade badges themselves were removed. */
.pg-num { font-size: 17px; font-weight: 900; letter-spacing: -0.5px; line-height: 1; }
.pg-verdict { font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; line-height: 1; }
.pg-ctx { font-size: 8px; color: var(--text-muted); line-height: 1; letter-spacing: 0.4px; }
/* Desktop (≥1024px): lay each price box out horizontally so Target Buy / Sell
   read as compact quote bars that use the full width. */
@media screen and (min-width: 1024px) {
  .price-strip { gap: 10px; }
  .price-box { display: flex; align-items: center; gap: 18px; padding: 10px 18px 10px 20px; }
  .price-label { flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; flex-shrink: 0; }
  .price-val { font-size: 28px; margin-top: 0; flex-shrink: 0; white-space: nowrap; letter-spacing: -0.7px; margin-left: auto; gap: 12px; }
  /* The hero cards were sharing .main-column's flat 8px gap with everything
     else in the column; a few px more separates the recommendation from the
     evidence for it. Only on the layout with height to spare. */
  .price-strip { margin-bottom: 5px; }
  /* The card this sits in is the roomiest one — take the space. */
  .price-step { width: 30px; height: 30px; font-size: 18px; border-radius: 7px; }
}
/* Tablet/narrow-window gap (641px–1023px): the phone-portrait rule below
   handles ≤640px and the desktop rule above handles ≥1024px, but this band
   in between fell through to the base (stacked label-then-value) styles,
   which put the value on its own row under the box's label — the exact
   "big number drops under Target Buy/Sell" layout shift that's unwanted at
   any width. Same horizontal single-row treatment here, just tuned tighter. */
@media screen and (min-width: 641px) and (max-width: 1023px) {
  .price-box { display: flex; align-items: center; gap: 10px; padding: 6px 12px; overflow: hidden; }
  /* Same inversion the phone block further down already fixes, which this band
     never got: label flex-shrink:0 + value flex-shrink:1 means the caption
     ("Insta-sell: 58,412,684 · 3m ago") takes what it wants and the number
     absorbs every pixel of the shortfall. Measured in a 225px box it left the
     target a clientWidth of ZERO — fitPriceNums had ground the font down to 9px
     and it still clipped, because no font size can fit into no space.
     The caption shrinks and ellipsises; the number never does. */
  .price-label { flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; flex-shrink: 1; min-width: 0; }
  .price-label .live-val { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .price-val { font-size: 20px; margin-top: 0; flex-shrink: 0; white-space: nowrap; letter-spacing: -0.5px; margin-left: auto; }
}
/* Medium-desktop band: viewports between the mobile breakpoint (1024px)
   and a comfortable wide-desktop width were overflowing for items with
   very large prices (e.g. Gilded platebody at 10.6M — 8-digit numbers
   at 28px overflowed the price box and clipped the grade chip). Cap the
   hero font at 22px and tighten gaps/padding in this band; everything
   above 1280px keeps the full 28px hero size. */
@media screen and (min-width: 1024px) and (max-width: 1279px) {
  .price-box { gap: 12px; padding: 8px 14px 8px 16px; }
  .price-val { font-size: 22px; letter-spacing: -0.4px; }
}
/* Compact timeframe pills, keyed to the HEADER's own width rather than the
   viewport's. Same six buttons, ~266px instead of ~462px — enough that they
   still share the name/Save row on a narrow window instead of needing a
   second line. It has to be a container query, not a media query: the header
   also gets narrow on a wide screen when the sidebar is dragged out, and a
   viewport breakpoint can't see that. 860px is the threshold because the
   widest measured title group (300px) plus Save, Share, gaps and the full-size
   462px toolbar needs about that much; below it, shrink the pills rather than
   the name. */
@container tickerhdr (max-width: 860px) {
  .ticker-header .toolbar { padding: 3px; gap: 4px; }
  .ticker-header .toolbar button { padding: 4px 6px; font-size: 13px; min-width: 0; }
  .ticker-header .toolbar button .pct-chg { font-size: 9px; }
}
/* Near-opaque (it lands on top of the scatter, and 0.95 let dots show
   through the numbers), warm-black to match the app rather than the old
   blue-grey, and tabular figures so prices don't jitter as the cursor
   moves along the series. */
.chart-tip { position: absolute; display: none; background: rgba(10, 9, 8, 0.97); color: var(--text-main); padding: 7px 9px; border-radius: 5px; font-size: 10px; line-height: 1.5; font-variant-numeric: tabular-nums; pointer-events: none; white-space: nowrap; border: 1px solid var(--border-light); box-shadow: 0 6px 18px rgba(0,0,0,0.6); z-index: 50; }
@media (min-width: 1024px) { .chart-tip { font-size: 11.5px; padding: 8px 11px; } }
.chart-tip strong { display: block; margin-bottom: 4px; padding-bottom: 4px; color: #fff; font-size: 0.92em; letter-spacing: .2px; border-bottom: 1px solid var(--border-main); }
.chart-tip .tip-marker { font-weight: 800; letter-spacing: 0.2px; }
/* align-self:flex-start + max-height:100% instead of the default stretch.
   Stretched, the panel was always the full height of the chart column, so a
   short favourites list left the panel drawn — border, background and all —
   over ~180px of nothing below Find Opportunities. Now the panel ends where
   its content ends, and only grows to the column's height when it has the
   content to fill it. max-height keeps the internal scrolling: at full height
   the list still shrinks and scrolls rather than overflowing.
   NOTE: this makes the panel's height indefinite, which is exactly what the
   splitter's `max-height: var(--opps-h, 46%)` resolves against — see the
   note on .find-opps-host. */
.watchlist-container { width: var(--wl-width, 330px); flex-shrink: 0; align-self: flex-start; max-height: 100%; background: var(--bg-panel); border: 1px solid var(--border-hair); border-radius: 6px; display: flex; flex-direction: column; overflow: hidden; }
/* Draggable divider between chart and sidebar. Hidden by default (real
   phones, where the sidebar stacks below the chart instead of beside it);
   shown any time the layout is the side-by-side row, which since the
   min-width:1024px gate isn't the only place that happens anymore (a
   wide-but-<1024px window falls into this same row layout on its base
   styles) starts at 640px, not 1024px. Explicitly re-hidden inside the
   short-landscape breakpoint below, where the sidebar is a fixed compact
   width rather than the --wl-width variable this drags. */
.wl-resize { display: none; }
/* Height counterpart to .wl-resize, on the two layouts where the chart's
   height is a fixed budget worth handing over: desktop (a viewport-derived
   min/max) and portrait phones (a flat 52vh, and on a phone the chart is
   exactly the thing you want more of). Coarse landscape is left out — the
   whole viewport is ~390px tall there, so there is nothing to trade.
   .user-sized replaces whichever rule that layout used.
   touch-action:none is what stops a drag from scrolling the page instead of
   resizing, which is the entire gesture on a phone. */
.ch-resize { display: none; }
.ch-resize::before { content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); height: 2px; width: 38px; background: var(--border-light); border-radius: 2px; transition: background 0.15s, width 0.15s; }
.ch-resize:hover::before, .ch-resize.dragging::before { background: var(--border-active); width: 64px; }
/* Only applied once the handle has actually been dragged, so the default
   sizing keeps working untouched for everyone else. */
.chart-container.user-sized { flex: 0 0 auto; height: var(--chart-h); min-height: 0; max-height: none; }
/* 641px, not 1024. The side-by-side row layout starts at 640 — .wl-resize,
   the WIDTH handle, already says so in its own comment — but the height
   handle was gated at 1024, so every window between the two had no way to
   resize the chart at all. Reported from an 878x842 window: display computed
   to `none` and there was nothing to drag.
   The landscape-phone rule further down is later in source and keeps its own
   14px sizing, so this does not reach it. */
@media (min-width: 641px) {
  .ch-resize { display: block; flex: 0 0 10px; height: 10px; margin: -4px 0; cursor: row-resize; position: relative; z-index: 6; touch-action: none; }
}
/* …and the band needs the same two supports the other layouts already have,
   or the handle would be present and useless.
   .wrap is a fixed 100dvh here (the min-height:100dvh variant is >=1024
   only), so a hand-sized chart would be clipped rather than pushing what is
   under it down. height:auto once user-sized, with no floor, so growing the
   chart grows the page AND shrinking it closes the gap up — the same net
   effect as the >=1024 rule, reached with one declaration because this band
   has no min-height to cancel. Reset drops .user-sized and 100dvh returns.
   The sidebar cap has to stop being a percentage for the same reason it did
   in every other layout: with .wrap's height indefinite, `max-height: 100%`
   resolves to `none` and a long favourites list grows the page instead of
   scrolling inside itself. */
@media (min-width: 641px) and (max-width: 1023.98px) {
  .wrap:has(.chart-container.user-sized) { height: auto; }
}
/* HEIGHT, not max-height: the panel is exactly as tall as the main column, so
   it always reaches the row the column ends on rather than stopping wherever
   its own content happens to run out. Capped by the viewport for the case
   where the column is taller than the screen. --main-h is measured in JS; see
   sidebarCap.
   >=641px, matching .ch-resize and .wl-resize rather than the 1024 this
   started at. The band between was the one place the sidebar could still be
   the taller column: measured at 878x842, shrinking the chart left the panel
   198px past the main column's last row — the same blank band the desktop
   rule exists to prevent. */
/* max(): the column's height is the TARGET, --fav-floor is the FLOOR. Slimmer
   rows alone cannot deliver "15 favourites before it scrolls" — measured, a
   900px window would need 30.8px rows and a 720px one 18.8px, which is a row
   shorter than the icon in it. The panel has to be allowed past the column
   instead, and only by as much as the rows actually present require: the
   floor is measured as chrome + min(favourites, 15) rows, so a short list
   still ends where its content ends and nothing re-opens the blank band the
   height rule exists to close. Desktop only; --fav-floor is unset elsewhere
   and max() then ignores it. */
@media (min-width: 641px) {
  .watchlist-container { height: max(min(calc(100dvh - 16px), var(--main-h, 100dvh)), var(--fav-floor, 0px)); }
}
/* Both touch layouts — portrait AND landscape phone. Landscape used to be
   the one band with no handle at all, on the reasoning that a ~390px-tall
   viewport has nothing to trade. That is true only while the app is pinned to
   one screen: the ask is to trade the quick-facts row and the 30-Day Range
   for chart, pushing them below the fold, which is exactly what the portrait
   handle already does. So the band gets the same handle and, in its own block
   further down, the same `height: auto` on .wrap that lets a taller chart
   grow the page instead of overflowing a fixed box. */
@media (max-width: 640px),
       screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  /* Taller than the desktop handle: a 10px strip is a fine mouse target and
     a poor thumb one. The pill inside stays 2px, so it costs 8px of layout
     and reads the same. */
  .ch-resize { display: block; flex: 0 0 20px; height: 20px; margin: -6px 0; cursor: row-resize; position: relative; z-index: 6; touch-action: none; }
  .ch-resize::before { width: 46px; height: 3px; }
  .ch-resize.dragging::before { width: 72px; }
}
/* ── Tap to maximise, alongside the drag ─────────────────────────────────
   Every touch layout gets the toggle: it offers the two heights that
   actually matter — the layout's default, and the same ceiling a full drag
   would reach — in one tap, which is a better gesture for "make this bigger"
   than a precise grab on a thin strip and a steady pull.
   It ADDS to the strip rather than replacing it. Hiding the strip on every
   coarse pointer was too wide a cut: portrait's handle sits 365px above the
   bottom edge and never collided with anything, and losing the fine control
   there was a straight loss.
   A mouse gets no button: a cursor drag is fine, and it already covers every
   height between the two the button offers.
   LAST of the .ch-resize rules on purpose — same specificity as the blocks
   above, so the one below wins on order. */
.ct-tall { display: none; }
@media (pointer: coarse) {
  .ct-tall { display: inline-flex; align-items: center; }
}
/* The one layout where the strip goes. Short landscape puts it ~48px above
   the bottom edge, inside iOS's home-indicator swipe band — reported as not
   being able to grab the handle without dismissing the app. Moving the grip
   left of the indicator made it reachable without making it pleasant, so the
   strip gives way to the button here and the off-centre pill that went with
   it is gone too: a 390px-tall screen also had the least room to spend 14px
   on chrome. Portrait and desktop keep the drag. */
@media screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  .ch-resize { display: none; }
}
@media (min-width: 640px) {
  .wl-resize { display: block; flex: 0 0 8px; align-self: stretch; cursor: col-resize; position: relative; margin: 0 -4px; z-index: 6; touch-action: none; }
  .wl-resize::before { content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 2px; height: 30px; background: var(--border-light); border-radius: 2px; transition: background 0.15s, height 0.15s; }
  .wl-resize:hover::before, .wl-resize.dragging::before { background: var(--border-active); height: 52px; }
}
@media (min-width: 1024px) {
  /* Desktop: let the page grow a hair past the viewport so the 30-day insight
     + description sit BELOW the chart instead of stealing its height. The chart
     is floored to fill the first screen down to the quick-facts strip (which
     then aligns with the sidebar's Favorites section); the insight lands just
     under the fold. Landscape/portrait keep their own height rules.
     The floor is what keeps the chart filling the first screen, and it only
     works because the chart's `flex: 1` has something to expand into. Drag
     the chart resizer and that stops being true — .chart-container takes an
     explicit --chart-h and leaves the flex sizing, so nothing in the column
     stretches any more. The floor then holds a full viewport open that
     nothing can fill, and the whole band between the app and the description
     goes blank. Measured at 1440x900 with the chart dragged to 190px and a
     short favourites list: content ended at 459, the description started at
     932 — 473px of nothing.
     See the :has() rule at the end of this block, which drops the floor in
     exactly that state. */
  .wrap { height: auto; min-height: 100dvh; }
  .content-area { align-items: stretch; }
  /* Cap the chart so header + price strip + chart + quick-facts fill the first
     screen; the insight/description card then starts just past the fold.
     min-height is a short-viewport safety floor only. (The card's own desktop
     styling lives after the base .ins-* rules so source order wins.) */
  /* The 248/320 pair left the 30-Day Range bar 80px below the fold at every
     desktop size measured — 1280x720 through 2560x1440, the same 80 every
     time, because the chart is the only elastic element and it was sized to
     end exactly at the fold with the range bar following it. That bar is the
     one thing on the page answering "is this cheap right now", and a new
     visitor had to scroll to discover it existed. 88px more (80 plus a little
     air) buys it back out of the chart, which has the most pixels to spare.
     Both numbers move together: min-height is a short-viewport safety floor
     and would win over max-height if it were left above it. */
  .chart-container { min-height: calc(100dvh - 408px); max-height: calc(100dvh - 336px); }
}
/* Just above the desktop breakpoint the quick-facts strip runs out of width
   and wraps, which costs another ~40px that the 336 above does not know
   about — measured at 1024x768, where the range bar still sat 38px under the
   fold while every wider size cleared it by 8. Narrower band, deeper cut. */
@media (min-width: 1024px) and (max-width: 1200px) {
  .chart-container { min-height: calc(100dvh - 456px); max-height: calc(100dvh - 384px); }
}
@media (min-width: 1024px) {
  /* Without this, a long favorites list had nothing to bound it against —
     .wrap's height is auto (that's what lets the insight card sit just past
     the fold above), so align-items:stretch just grew the WHOLE page to fit
     every favorite instead of scrolling the list internally. That pushed
     "Find Opportunities" (and the tail of a long list) below the viewport,
     reachable only by scrolling past the chart entirely. Capping the sidebar
     to ~one screen makes .watchlist-content's existing overflow-y:auto
     actually engage once the list runs long, so it scrolls on its own while
     the chart stays put. */
  /* --main-h is the MAIN COLUMN's content height, measured in JS (see
     sidebarCap). The viewport figure is the fallback and the hard ceiling:
     before this, an expanded Find Opportunities grew the sidebar to a full
     screen while the main column ended well above it — the panel ran ~180px
     past the "VOL 1.47M/day" row it sits beside, and expanding a scanner
     shoved the whole page down. Capped to the column, the scanner scrolls
     inside its own box instead and nothing else moves. */
  /* The panel's own height rule moved to its own >=641px block below, so the
     641-1023 band gets it too — see there. */
  /* Who absorbs the difference, and who gives it back. The favourites list
     GROWS into whatever the panel has spare — so a short list simply has room
     under its rows, which is what a watchlist looks like, rather than leaving
     a hole between two panels. Find Opportunities takes what it needs and
     yields 100x faster, so when the two together overflow it is the scanner
     that scrolls, not the list: "my favourite list shouldn't scroll and
     shorten like that, just the Find Opportunities".
     min-height keeps its title bar from being squashed to nothing when it is
     the one giving ground. Only once it is down to that bar does the list
     start to scroll, which with a long enough list is unavoidable. */

  /* .wrap is `height: 100dvh` so the chart's `flex: 1` has a definite screen
     to fill — the app deliberately occupies exactly one viewport. Drag the
     chart resizer, though, and .chart-container leaves that flex sizing for
     an explicit `--chart-h`: nothing in the column stretches any more, both
     columns end at their content, and the leftover shows as a blank band
     between the app and the about-section that nothing can move into.
     Measured at 1589x526 with the chart dragged to 190px: the quick-facts
     row ended at 420 while .main-column ran to 518, so ~98px of empty column
     inside a .wrap that still claimed the whole screen. At 900 tall it was
     172px.
     Shrinking the wrap in exactly that state is the honest reading of the
     gesture: making the chart smaller should mean less app on screen, not
     the same screen with a hole in it. Everything else keeps 100dvh, so the
     default layout is untouched.
     Safe here specifically because the sidebar's cap above is viewport-based
     (calc(100dvh - 16px)) rather than a percentage — with nothing definite to
     resolve against, a `max-height: 100%` would compute to `none` and a long
     favourites list would grow the page instead of scrolling internally.
     min-height, NOT height: `height` is already `auto` two rules up, so
     setting it again does nothing — the floor is the whole mechanism, and
     the first version of this change set the wrong one and measured no
     difference at all.
     :has() is unsupported on very old browsers, where this simply doesn't
     match and the behaviour is what it is today. */
  .wrap:has(.chart-container.user-sized) { min-height: 0; }
}
.calc-container { background: var(--bg-panel); border-bottom: 1px solid var(--border-main); padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; }
.calc-headline { display: flex; justify-content: space-between; align-items: center; cursor: pointer; gap: 8px; border-radius: 4px; padding: 2px; margin: -2px; transition: background 0.15s ease; -webkit-tap-highlight-color: rgba(201,166,77,0.2); }
/* Row-level hover/press, shared by all three section headers. Only
   Potential Profit had this, because only its row was clickable — so
   hovering near its chevron lit the whole row while the identical-looking
   Analyst Rating and Recommended Flip headers did nothing. The other two
   are click targets now (see wireHeaderToggle), which makes the highlight
   honest as well as consistent, and turns two 22px chevrons into
   full-width targets on a phone. */
.calc-headline:hover, .rg-top.is-toggle:hover, .fc-head.is-toggle:hover { background: rgba(255,255,255,0.04); }
.calc-headline:active, .rg-top.is-toggle:active, .fc-head.is-toggle:active { background: rgba(201,166,77,0.12); }
.rg-top.is-toggle, .fc-head.is-toggle {
  cursor: pointer; border-radius: 4px; padding: 2px; margin: -2px;
  transition: background 0.15s ease; -webkit-tap-highlight-color: rgba(201,166,77,0.2);
}
.calc-headline-left { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
/* Structural, same as the other section labels — it names the block, the
   green figure beside it carries the signal.
   Shares the ONE sidebar-header recipe (see --hdr-* below): every section
   label in the sidebar is 11px / 800 / 0.7px / --text-structural / upper,
   They used to be four different sizes (9, 10.5, 11, 13) in two colours,
   which made a column of equal-rank section names read like a ranking. */
.calc-headline-label {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--hdr-size); font-weight: var(--hdr-weight);
  color: var(--text-structural); text-transform: uppercase;
  letter-spacing: var(--hdr-track);
}
.calc-headline-sub { font-size: 9px; color: var(--text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.calc-headline-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.calc-headline .res-profit { font-size: 16px; font-weight: bold; white-space: nowrap; }
@media (min-width: 1024px) {
  .calc-headline { padding: 4px; }
  .calc-headline-sub { font-size: 10px; }
  .calc-headline .res-profit { font-size: 22px; letter-spacing: -0.5px; }
}
/* The three section expanders — Potential Profit, Analyst Rating and
   Recommended Flip — all wear this class, so the hover and press live HERE
   rather than on each one. They had drifted to three different behaviours:
   Analyst Rating had its own :hover, Potential Profit only lit up because
   its whole header row does, and the flip card's chevron had nothing at
   all, which is what made it feel broken rather than merely quiet. */
.calc-caret { font-size: 13px; color: var(--border-active); width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; border-radius: 4px; background: rgba(201,166,77,0.12); flex-shrink: 0; cursor: pointer; transition: background .15s ease, color .15s ease, transform .12s ease; }
.calc-caret:hover { background: rgba(201,166,77,0.26); color: #FFD98A; }
.calc-caret:active { transform: translateY(1px); }
.calc-body { margin-top: 10px; }
.calc-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.calc-container input { width: 100%; background: var(--bg-input); border: 1px solid var(--border-light); color: var(--text-main); border-radius: 4px; padding: 6px 8px; font-size: 12px; outline: none; font-family: inherit; }
.calc-input label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; }
.calc-container input:focus { border-color: var(--border-active); }
/* Compact breakdown, not a block of body text.
   Every row used to be 10px/400 in the same muted colour — measured
   identical, 11px tall each — so label and number carried exactly the same
   weight and the whole thing read as four lines of prose you had to parse
   end to end to find the one number you came for. Three tiers now:

     labels    9px, muted, letterspaced — row keys, not sentences
     numbers   11px, --text-main, tabular — the thing you actually read
     asides    the repeated "gp" and the "2%, cap 5M" / "x10,000"
               qualifiers, dimmer than either, because they say the same
               thing on every row and carry no value on their own

   Row gaps drop 3px -> 1px so the working rows sit as a tight block, and
   that reclaimed space goes to the two rows that aren't part of the
   subtraction: Capital needed above, Net profit below, each cut off by a
   hairline. Net profit also takes a size step, so it terminates the
   breakdown rather than being the last of several identical lines
   distinguished only by colour.
   Rows carry an explicit 13px line box so mixing 9px and 11px inside one
   row doesn't let the taller glyph push the row height around. */
.calc-breakdown { margin-top: 8px; color: var(--text-muted); display: flex; flex-direction: column; gap: 1px; background: rgba(0,0,0,0.25); padding: 6px 10px 7px; border-radius: 4px; border: 1px solid var(--border-main); }
/* align-items:center, not baseline: the label span needs overflow:hidden for
   its ellipsis on narrow phones, and an overflow-clipped flex item
   synthesises its baseline from the bottom margin edge instead of its text,
   which would drop the label below the number. Both spans share the row's
   explicit line box, so centring the equal-height boxes is stable. */
.calc-breakdown .row { display: flex; justify-content: space-between; align-items: center; gap: 12px; line-height: 12px; }
.calc-breakdown .row > span:first-child { font-size: 9px; letter-spacing: 0.2px; color: var(--text-muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calc-breakdown .row > span:last-child { font-size: 11px; color: var(--text-main); font-variant-numeric: tabular-nums; white-space: nowrap; flex-shrink: 0; }
/* Qualifier inside a label, and the unit after a number. Both are dimmer
   than the muted label tint — decorative repetition, present for the
   reader who wants it and invisible to the one who doesn't. */
.calc-breakdown .bd-q { color: rgba(217,211,199,0.34); letter-spacing: 0; }
.calc-breakdown .bd-u { font-size: 9px; color: rgba(217,211,199,0.34); margin-left: 3px; font-weight: 400; }
/* The stake sits above its own hairline for the same reason Net profit sits
   below one: neither is a term in the margin-minus-tax sum between them, and
   an unsigned 2.4B parked in a column of signed numbers otherwise invites the
   reader to add or subtract it. */
.calc-breakdown .bd-stake { margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid var(--border-light); }
.calc-breakdown .bd-net { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border-light); line-height: 15px; }
.calc-breakdown .bd-net > span:first-child { font-size: 10px; letter-spacing: 0.3px; color: var(--text-main); }
.calc-breakdown .bd-net > span:last-child { font-size: 13px; font-weight: 700; }
/* Content-sized, not greedy. `flex: 1` made this list claim every spare
   pixel of the column, so a short favourites list padded itself out with
   empty scroll area and pushed the splitter and Find Opportunities to the
   floor — measured on an 11-item list in a 936px column, 568px of nothing
   between the last row and the divider. The list is as tall as its rows now
   and Find Opportunities sits directly under them.
   shrink:100 rather than 1, so when the column IS over-full this is the
   thing that gives. Its siblings (the flip card, the two headers) are all
   `0 1 auto` and would otherwise squash proportionally — a half-height flip
   card to save a list that can simply scroll. min-height:0 is what lets it
   shrink below content at all; overflow-y:auto is what it does then. */
/* shrink:1, and the modules above are pinned instead — see .flip-finder et al
   below. Was shrink:100, from when this list was the only thing that could
   give: with the panel now capped to the main column, expanding a scanner
   made the list yield ALL of it and the favourites vanished entirely. Equal
   shrink lets the two lists give ground in proportion to their content, so
   both stay visible and both scroll, and the splitter is still there to
   override the split deliberately. */
/* GROWS into whatever the panel has spare, and is the LAST thing to give it
   back. A short list simply has room under its rows — which is what a
   watchlist looks like — rather than leaving a hole between two panels, and a
   list that fits is never scrolled just because the scanner below it is open.
   Pair this with .find-opps-host's shrink:100 below; the two together are the
   whole rule, and changing one without the other is what made the favourites
   collapse to a single row. */
/* min-height is the list's RESERVE, not zero, whenever the floor is in play
   (see sidebarCap). The panel's height already sets aside chrome + up to 15
   rows, but reserving it in the panel is not the same as the list keeping it:
   flex hands the scanner shrink:100 against the list's shrink:1, and
   proportional shrink still took ~3px off the list — enough to clip the
   eleventh row and start it scrolling while the scanner sat 3px above its
   own floor. With the reserve declared, the scanner yields all of it and
   scrolls, which is the rule everywhere else on this panel. Defaults to 0,
   so every layout that does not set --fav-min is exactly as it was. */
.watchlist-content { flex: 1 1 auto; overflow-y: auto; min-height: var(--fav-min, 0px); max-height: var(--fav-h, none); }
/* The fixed-content siblings do NOT shrink. This is what shrink:100 on the
   list was really protecting against — a half-height Recommended Flip to save
   a list that can simply scroll — and pinning them says it directly instead
   of encoding it as a ratio between two other numbers. */
.flip-finder, .fav-head-host, .col-header { flex: 0 0 auto; }
/* ── Splitter between Favorites and Find Opportunities ───────────────────
   The two lists share one fixed-height column, so every px one gains the
   other loses — which is exactly the case a draggable divider exists for.
   Hidden by default and switched on only where the column's height is
   actually constrained (see the desktop block): on phone portrait the whole
   sidebar scrolls, so there is no fixed budget to divide and a splitter would
   be a control that does nothing.
   The hit area is 11px but the visible line is 3px — a 3px drag target is a
   fiddly one, and the padding costs nothing because it sits in the gap the
   two sections already had between them. */
.wl-splitter { display: none; }
@media (min-width: 1024px) and (pointer: fine) {
  .wl-splitter {
    display: flex; align-items: center; justify-content: center;
    height: 11px; flex: 0 0 auto; cursor: row-resize;
    touch-action: none;                 /* we own the gesture; don't scroll */
    background: transparent; border: 0; padding: 0;
  }
  .wl-splitter span {
    display: block; height: 3px; width: 46px; border-radius: 2px;
    background: rgba(255,255,255,0.13);
    transition: background 0.12s ease, width 0.12s ease;
  }
  .wl-splitter:hover span, .wl-splitter:focus-visible span, .wl-splitter.is-dragging span {
    background: var(--border-active); width: 64px;
  }
  .wl-splitter:focus-visible { outline: none; }
  /* Sized by the drag, scrolls its own overflow, and never grows past the
     column. max-height rather than height so a COLLAPSED Find Opportunities
     stays the height of its own title bar instead of being padded out to a
     size the user picked for the expanded state. */
  /* No max-height of its own any more. This element used to carry the
     splitter's cap (--opps-h); the splitter sizes the favourites list
     directly now, and this simply follows it up the column. `0 1 auto` with
     min-height:0 so that when both lists together exceed the panel it can
     still give ground — the list shrinks 100x faster (see .watchlist-content),
     so this only yields once the list is already at its floor. */
  /* Takes what it needs and yields 100x faster than the list, so when the two
     together overflow the panel it is the SCANNER that scrolls. The floor
     keeps its title bar intact while it is the one giving ground — measured
     without one, a 60-item list squeezed this to 7px, i.e. the section header
     itself was sliced. Only once it is down to that bar does the list start
     to scroll, which with a long enough list is unavoidable.

     That floor was a flat 32px while the bar measures 35px plus its 1px
     rule, so the scanner was squeezed 4px past its own header: the closed
     bar got a scrollbar of its own and a sliced header — a control with
     nothing inside it offering to scroll. 36px is the measured height, not
     another guess.
     `min-height: min-content` looked like the way to stop guessing
     altogether, and is not: heights have no min-content/max-content
     distinction the way widths do, so it resolves to the FULL content
     height and the scanner grew to 1431px and swallowed the whole panel.
     The number stays; what makes it safe is the rule below. */
  .find-opps-host {
    flex: 0 100 auto; min-height: 36px;
    overflow-y: auto; overscroll-behavior: contain;
  }
  /* Closed, the floor stops mattering: content-sized and unshrinkable, so
     the host is exactly the bar however tall the bar happens to be. The
     state that has nothing to scroll can no longer be squeezed into
     scrolling it, whatever --hdr-size does at some future breakpoint —
     which is the half of this that isn't a magic number. */
  .find-opps-host:has(.find-opps-body.collapsed) { flex: 0 0 auto; overflow: hidden; }
  /* While dragging, kill text selection and pointer noise across the whole
     page — without this a fast drag selects list rows behind the cursor. */
  body.wl-resizing { user-select: none; cursor: row-resize; }
}
.category-group { border-bottom: 1px solid var(--border-main); }
/* Category title — sticky at top of the scroll container AND visually
   distinct from the dense colored rows below.
   Previous attempt went darker than the panel and kept colored text; both
   blended into the data. Now: LIGHTER bg than rows (clear "this is a
   different layer" contrast), always-bright white text (doesn't compete
   with the colored row badges), and a fat 3px colored accent stripe on the
   left so each section is still identifiable by color WITHOUT making the
   title text itself harder to read. */
.category-title {
  --cat-accent: var(--border-active);
  position: sticky; top: 0; z-index: 4;
  padding: 6px 12px 6px 13px;
  font-size: var(--hdr-size); font-weight: var(--hdr-weight); letter-spacing: var(--hdr-track);
  text-transform: uppercase;
  color: var(--text-structural);
  /* Flat panel fill, NOT a raised chip. This used to be a top-lit gradient
     (#312b23 -> #231e18) with a bevel highlight and a drop shadow, which
     made Favorites and Find Opportunities read as bolder and more important
     than Potential Profit, Analyst Rating and Recommended Flip — three
     headers of identical rank that are plain text on the panel. Same font
     the whole time; the weight people saw was the chrome around it.
     The fill has to stay OPAQUE because this header is sticky and list rows
     scroll underneath it, but it can be the panel's own colour, which is
     exactly what sits behind the other three. Section identity now rides
     entirely on the left accent stripe. */
  background: var(--bg-panel);
  border-left: 3px solid var(--cat-accent);
  border-bottom: 1px solid #141210;
  cursor: pointer;
  display: flex; justify-content: space-between; align-items: center;
  box-shadow: inset 6px 0 20px -15px var(--cat-accent);
  transition: background 0.15s ease, box-shadow 0.15s ease;
}
/* Per-section accent colour drives the left stripe AND the left-edge glow. */
.category-title.high-title { --cat-accent: var(--sell-color); }
.category-title.low-title { --cat-accent: var(--buy-color); }
/* Parchment, not fav-gold. Gold is the app's "you chose this / act on this"
   colour — the saved star, the active timeframe, hover states. Spending it on
   a section stripe that is permanently there made the loudest accent in the
   sidebar mean nothing in particular. Parchment says "section" without
   claiming to be a signal. Find Opportunities takes the same one below, so the
   two headers read as siblings rather than as two unrelated colour codes. */
.category-title.fav-title { --cat-accent: var(--rs-parchment, #F2D49B); }
.category-title.lose-title { --cat-accent: var(--negative); }
.category-title.rec-title { --cat-accent: #FF9800; }
/* Muted sage greens — the old #2ECC71/#7FE3A6 limes read neon-cool against
   the warm obsidian panels. */
.category-title.margin-title { --cat-accent: #57B87B; }
.category-title.marginlo-title { --cat-accent: #9BD4AE; }
.category-title.steady-title { --cat-accent: var(--rs-parchment); }
.category-title.fall-title { --cat-accent: var(--negative); }
/* "Find Opportunities" wrapper — same title chrome as any other section,
   just with a neutral accent (it's a container for six differently-
   accented sections, not a signal of its own). */
.category-title.find-opps-title { --cat-accent: var(--rs-parchment, #F2D49B); }
/* Nested scanner titles (inside "Find Opportunities") are visually
   subordinate to it: smaller, indented, and NOT sticky — the outer
   wrapper's title already stays pinned while scrolling, so a second sticky
   header one level down would just fight it for the same top:0 slot.
   ONE quiet row style for all six, and no per-row accent stripe. Each scanner
   used to carry its own colour (sage, mint, parchment, red, teal, gold), which
   is right for a top-level section and wrong here: six coloured bars stacked
   under one header read as six competing sections rather than one menu, and
   they out-shouted Favorites directly above them despite ranking below it.
   The section accent on "Find Opportunities" is the one that carries identity;
   these are its contents. The stripe is kept as a transparent 3px so the text
   stays on the same left edge as every other title — collapsing the border
   would shift all six sideways relative to the header they sit under. */
.category-title.nested {
  position: static;
  padding: 6px 12px 6px 24px;
  font-size: 10px;
  --cat-accent: transparent;
  border-left-color: transparent;
  box-shadow: none;
  color: var(--text-muted);
}
.category-title.nested:hover {
  background: var(--bg-input);
  box-shadow: none;
  color: var(--text-main);
}
/* Quieter chevron to match — same idiom, one step down in contrast, so the
   hierarchy reads on the control as well as the label. */
.category-title.nested .cat-chevron {
  width: 20px; height: 20px;
  background: rgba(201,166,77,0.07);
  color: rgba(201,166,77,0.75);
}
.category-title.nested:hover .cat-chevron { background: rgba(201,166,77,0.2); color: #FFD98A; }
.category-title.nested .cat-chevron .ui-ic { width: 12px; height: 12px; }
.category-title:hover {
  background: linear-gradient(180deg, #3b342a 0%, #2a241d 100%);
  box-shadow: inset 8px 0 24px -15px var(--cat-accent),
              inset 0 1px 0 rgba(255,255,255,0.08),
              0 2px 8px -3px rgba(0,0,0,0.6);
}
/* Chevron rendered as a clearly tappable button so users can find "close"
   without aiming at a tiny glyph. Inverts when the section is collapsed so
   it's even more obvious the title is interactive. */
/* Same chrome as .calc-caret, deliberately. The note below claims one chevron
   idiom everywhere, but that only ever covered the GLYPH and its rotation —
   the chrome had quietly forked into two: a gold-tinted borderless chip on
   Potential Profit / Analyst Rating / Recommended Flip, and a white-tinted
   bordered box on the category headers. Both sit in the same sidebar, inches
   apart, doing the identical job, so the difference read as an accident rather
   than a distinction. Standardised on the gold chip: three of the five already
   used it, and gold is the app's interactive accent everywhere else. */
.cat-chevron {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  font-size: 13px; color: var(--border-active);
  background: rgba(201,166,77,0.12);
  border: 0;
  border-radius: 4px;
  flex-shrink: 0;
  transition: background 0.12s ease, color 0.12s ease;
}
.category-title:hover .cat-chevron { background: rgba(201,166,77,0.26); color: #FFD98A; }
/* ONE chevron idiom everywhere something expands/collapses (category
   headers, Potential Profit, flip-card): the same SVG chevron-down that
   rotates -90° when closed, in the same gold chip — glyph, rotation AND
   chrome, since matching only the first two still looked like two systems.
   The one chevron that stays different is .fls-caret on the Favorites title,
   and that is on purpose: it does not collapse anything, it opens the
   watchlist switcher. Bare glyph, no chip, and it rotates 180° rather than
   -90°, so the two roles stay distinguishable at a glance. */
.cat-chevron .ui-ic, .calc-caret .ui-ic { width: 13px; height: 13px; transition: transform 0.15s ease; }
.cat-chevron.closed .ui-ic, .calc-caret.closed .ui-ic { transform: rotate(-90deg); }

.category-list { display: block; }
.category-list.collapsed { display: none; }
.wl-page-link { display: flex; align-items: center; gap: 6px; padding: 8px 12px; font-size: 11.5px; color: var(--text-muted); text-decoration: none; border-top: 1px solid var(--border-light); }
.wl-page-link:hover { color: var(--accent-bright); }
.wl-page-link .ui-ic { width: 12px; height: 12px; }
/* Favorites list-switcher — TradingView-watchlist-header style: lives
   INSIDE the section title itself (no separate row, no collapse chevron —
   there's never a reason to collapse Favorites, only to switch which list
   is showing). The title carries a "[list name] ⌄" button
   that opens a dropdown of every list; a "+" replaces the chevron slot on
   the right to create one. */
/* Its dropdown menu can visually overflow below the title's own box, into
   where the next sticky section title sits. Both this title and that one
   are position:sticky + z-index (each forms its own stacking context), so
   a high z-index on the MENU alone can't win — it's trapped inside this
   title's stacking context. Raising the title's own z-index above the
   default 4 makes the whole context (title + its overflowing menu) paint
   above the next title instead of losing to it on DOM order. */
.category-title.fav-title { cursor: default; z-index: 10; }
/* Plain text, not a pill-in-a-pill — matches every other section title's
   size/weight/tracking exactly (13px read oversized and heavy next to
   "HIGH VOL MARGINS" etc. below it), with only a color shift + caret to
   signal it's interactive. */
.fls-current {
  display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 200px;
  background: none; border: none; padding: 0; margin: 0;
  /* The header TOKENS, not hand-copied values. This is the FAVORITES title,
     one of six section headers that are meant to be identical — and it was
     the only one at weight 800, because it is a <button> (the list switcher)
     rather than the .category-title the other list header uses, so it never
     picked up --hdr-weight when the headers were unified. 11px/0.7px matched
     by luck; the weight did not. */
  color: var(--text-structural); font-size: var(--hdr-size); font-weight: var(--hdr-weight);
  letter-spacing: var(--hdr-track); text-transform: uppercase;
  cursor: pointer; transition: color 0.12s ease;
}
.fls-current:hover, .fls-current.open { color: #E4E6EE; }
.fls-current .fls-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fls-current .fls-caret { display: flex; flex-shrink: 0; }
.fls-current .fls-caret .ui-ic { width: 15px; height: 15px; transition: transform 0.15s ease; }
.fls-current.open .fls-caret .ui-ic { transform: rotate(180deg); }
/* Bare glyph on the RIGHT of the row, beside the overflow menu — the
   TradingView arrangement. It used to be a bordered chip wedged between the
   list name and its caret, which put an action in the middle of a label and
   made the switcher look like it had two controls. Grouping it with the ⋯
   reads as "things you can do to this list", leaving the left side to say
   only which list you are on.
   No box, matching the share icon: colour carries the affordance. Parchment
   was too loud at rest — a bright glyph beside a muted label made the PLUS the
   first thing read in a row whose subject is the word FAVORITES. It takes the
   label's own --text-structural now, so the two sit at one level and the eye
   lands on the name. Weight 300 and a slightly larger size keep the glyph
   legible without the bold stroke doing the shouting instead of the colour.
   Gold on hover and focus, where being the loudest thing IS the point.
   Padding keeps a 22px target around a small glyph. */
.fls-add {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 22px; height: 22px; margin-left: auto; border-radius: 4px;
  background: transparent; border: 0; color: var(--text-structural);
  cursor: pointer; font-size: 17px; font-weight: 300; line-height: 1;
  transition: color 0.12s ease;
}
.fls-add:hover, .fls-add:focus-visible { color: var(--fav-gold, #FFB300); background: transparent; }
.fls-menu {
  display: none; position: absolute; top: 100%; left: 13px; z-index: 20;
  min-width: 190px; max-width: 260px; max-height: 260px; overflow-y: auto;
  background: #241f19; border: 1px solid var(--border-light); border-radius: 6px;
  box-shadow: 0 10px 30px -8px rgba(0,0,0,0.6);
  padding: 4px;
}
.fls-menu.open { display: block; }
.fls-menu-row { display: flex; align-items: center; gap: 2px; padding: 6px 6px; border-radius: 4px; font-size: 12px; color: var(--text-main); }
.fls-menu-row:hover { background: var(--bg-input); }
.fls-menu-row.active { color: var(--fav-gold, #FFB300); }
.fls-menu-row .fls-menu-name { flex: 1 1 auto; min-width: 0; cursor: pointer; padding: 2px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fls-menu-row .fls-rename, .fls-menu-row .fls-delete {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 18px; height: 18px; border: none; background: transparent; color: inherit;
  opacity: 0; cursor: pointer; border-radius: 3px; padding: 0; font-size: 13px; line-height: 1;
}
.fls-menu-row:hover .fls-rename, .fls-menu-row:hover .fls-delete { opacity: 0.65; }
.fls-menu-row .fls-rename:hover, .fls-menu-row .fls-delete:hover { opacity: 1; background: rgba(255,255,255,0.14); }
.fls-menu-row .fls-rename .ui-ic { width: 11px; height: 11px; }
/* Touch devices can't hover — always show the rename/delete affordances. */
@media (hover: none) {
  .fls-menu-row .fls-rename, .fls-menu-row .fls-delete { opacity: 0.55; }
}
.fls-menu-row.editing { padding: 3px; }
.fls-menu-input {
  width: 100%; background: var(--bg-input); border: 1px solid var(--border-active);
  color: var(--text-main); border-radius: 4px; padding: 5px 7px; font-size: 12px;
  outline: none; font-family: inherit;
}
.scan-row { padding: 10px 14px; font-size: 11px; color: var(--border-active); cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 6px; background: rgba(201,166,77,0.07); }
.scan-row:hover { background: var(--bg-input); }
.scan-row.loading { color: var(--text-muted); cursor: default; }
/* No per-row divider — a little extra vertical padding plus the hover fill
   below is enough separation without a line between every single row. */
/* 7px of vertical padding, not 10. A 41px row on a 900px-tall window let the
   list show 11 favourites; 35px shows 13, and the row still clears its 21px
   of content comfortably — the icon sets the floor here, not the padding. */
.wl-item { display: flex; align-items: center; justify-content: space-between; padding: 7px 14px; cursor: pointer; font-size: 13px; transition: background 0.2s; }
.wl-item:hover { background: var(--bg-input); }
/* Selected row uses a white OUTLINE (not a border) so it paints on top of the
   at-high / at-low green/gold borders + pulse box-shadow and reads identically
   on every list. outline-offset pulls it just inside the row edge. */
/* "You are here", not an alarm. A 2px pure-white outline was the highest
   contrast object in the sidebar — brighter than the 5D badges, the row
   accents and the prices — for a row whose only news is that you are
   already looking at it. A 1px hairline at 42% plus a slightly stronger
   glass fill still reads as selected at a glance without competing with
   the signals around it. */
.wl-item.active { background: rgba(255,255,255,0.07); outline: 1px solid rgba(255,255,255,0.42); outline-offset: -1px; }
/* Drag-to-reorder: the dragged row dims in place, and a thin gold line on
   whichever edge the cursor is nearer shows exactly where it'll land — no
   extra DOM element between rows, just an inset box-shadow on the row
   being hovered. */
.wl-item.dragging { opacity: 0.35; touch-action: none; }
/* Picked up by a long-press: lift it off the list so the hold clearly
   "took" before the finger has moved anywhere. */
.wl-item.dragging { box-shadow: 0 6px 18px rgba(0,0,0,.55); }
/* White, not the gold accent — a 5D-flagged row is already gold-tinted, and
   the indicator needs to read against any row color, not just the plain
   ones. */
.wl-item.drop-above { box-shadow: inset 0 2px 0 0 #fff; }
.wl-item.drop-below { box-shadow: inset 0 -2px 0 0 #fff; }
/* Day-high / Day-low highlight — RuneScape-styled so the "take action" cue
   reads at a glance: bright RS-green or RS-gold fill, double borders, the
   item name flipped to a bright callout color, plus a slow inset pulse so it
   draws the eye without being annoying. */
/* Day-tier rows: subtle tint + thin colored borders, no pulse — a soft cue
   that the price is at its 24h edge without competing with the 5D-tier rows. */
/* Highlight rows carry a LEFT accent bar plus a flat low tint, not a bright
   wash across the row. The 5D tier used to ramp to 36% of a fully saturated
   green and pulse the whole row, which glared badly in a dark list and made
   the numbers on top of it harder to read than the ones on a plain row —
   the opposite of what a highlight is for. The accent bar carries the
   signal; the tint just says "this row". */
/* Tint is neutral glass on every tier — the ACCENT BAR is the whole signal.
   A coloured wash meant two rows flagged for different reasons rendered as
   two different-coloured blocks, and the numbers printed on top of them
   inherited that cast; a 3px bar says the same thing without touching the
   data beside it. */
.wl-item.at-high {
  background: rgba(255,255,255,0.03);
  border-left: 3px solid rgba(79,255,142,0.55);
}
.wl-item.at-low {
  background: rgba(255,255,255,0.03);
  border-left: 3px solid rgba(255,210,77,0.55);
}
/* Selected at-high / at-low rows keep their green/gold gradient + the white
   .active outline (painted on top) carries the "selected" cue. */
/* Trimmed: at 8px/5px padding this chip was eating enough of the row that
   "Uncut emerald" truncated to "Uncut e...". The row's job is naming the
   item; the badge is an annotation on it. */
.hl-badge { font-size: 7.5px; font-weight: 900; padding: 1px 3px; border-radius: 2px; letter-spacing: 0.2px; line-height: 1.3; white-space: nowrap; flex-shrink: 0; }
/* Day-tier badges are deliberately PALE so the eye doesn't treat a routine
   intraday wiggle as a major signal. Outlined chip, muted fill, no glow. */
.hl-badge.high { background: rgba(79,255,142,0.18); color: #6FE8A0; border: 1px solid rgba(79,255,142,0.55); }
.hl-badge.low  { background: rgba(255,210,77,0.18); color: #E5B842; border: 1px solid rgba(255,210,77,0.55); }
/* 5D tier — fully saturated fill + glow so a real multi-day extreme pops
   sharply against the muted day-tier chips. Bright = notable. */
.hl-badge.high5d { background: #00FF7A; color: #001a0e; box-shadow: 0 0 10px rgba(0,255,122,0.7); }
.hl-badge.low5d  { background: #FFB300; color: #1a1100; box-shadow: 0 0 10px rgba(255,179,0,0.7); }
/* 5D tier: same shape as the day tier, one step up in weight — a solid
   accent bar at full signal colour and a slightly stronger tint. The pulse
   moved off the row background and onto the bar's glow, so the "this is
   live" motion survives without the whole row breathing. */
/* 5D tier: same neutral glass, same 3px bar — but the bar runs at FULL
   signal colour where the day tier's is 55% alpha, and only this tier
   pulses. Tier is read off the bar's intensity and the badge, not off how
   much of the row is stained and not off the name's weight. */
.wl-item.at-high5d {
  background: rgba(255,255,255,0.03);
  border-left: 3px solid #00FF7A;
  --sig-glow: 0,255,122;
}
.wl-item.at-low5d {
  background: rgba(255,255,255,0.03);
  border-left: 3px solid #FFB300;
  --sig-glow: 255,179,0;
}
/* The glow is a real light source now, not a shadow.
   It used to be `inset 5px 0 8px -5px` breathing to `9px 0 12px -5px` — a
   -5px spread on an 8px blur leaves about three visible pixels of haze
   against the bar, which is why it read as "is that even animating?". This
   is instead a wide gradient wash bleeding rightward off the bar, fading to
   nothing well before the row's numbers, so the bar looks like it is
   actually emitting light into the row. The glow can only travel right:
   .watchlist-container is overflow:hidden and the bar sits on the row's
   left edge, so any outward bloom would just be clipped away.
   4.6s, not 2.6s — a slow breath reads as "alive", a quick one reads as a
   notification badge demanding to be dismissed, and these rows sit on
   screen for as long as the list is open. */
.wl-item.at-high5d::before, .wl-item.at-low5d::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 40px;
  z-index: 0; pointer-events: none;
  background: linear-gradient(90deg,
    rgba(var(--sig-glow), 0.38) 0%,
    rgba(var(--sig-glow), 0.13) 50%,
    rgba(var(--sig-glow), 0) 100%);
  box-shadow: inset 7px 0 16px -9px rgba(var(--sig-glow), 1);
  animation: rs-glow-breathe 4.6s ease-in-out infinite;
}
/* The wash is a positioned child, so without this it would paint over the
   item name and price rather than behind them. */
.wl-item-left, .wl-item-right { position: relative; z-index: 1; }
@keyframes rs-glow-breathe {
  0%, 100% { opacity: 0.40; }
  50%      { opacity: 1; }
}
/* Spike tier — a genuine ±15%+ move (see SPIKE_THRESHOLD_PCT-equivalent in
   updateWatchlist), rare enough to earn a full border instead of the 5D
   pulse's side accent, cycling through a small palette rather than one
   color — OSRS's own rare-item shimmer (party hats, 3rd age), biased
   green/gold for a spike UP and red/magenta for a spike DOWN so the color
   itself still says which way it moved. Takes priority over the plain 5D
   pulse when both would apply. */
/* 2.6s, not the 1.4s it ran at: three colour stops in 1.4s is a strobe, and
   the spike tier already earns attention from the full border. */
.wl-item.at-spike-up {
  border: 2px solid #10B981;
  animation: rs-spike-up 2.6s linear infinite;
}
.wl-item.at-spike-down {
  border: 2px solid #EF5350;
  animation: rs-spike-down 2.6s linear infinite;
}
@keyframes rs-spike-up {
  0%   { border-color: #10B981; box-shadow: 0 0 12px rgba(16,185,129,0.6); }
  33%  { border-color: #4FFF8E; box-shadow: 0 0 12px rgba(79,255,142,0.6); }
  66%  { border-color: #FFD24D; box-shadow: 0 0 12px rgba(255,210,77,0.6); }
  100% { border-color: #10B981; box-shadow: 0 0 12px rgba(16,185,129,0.6); }
}
@keyframes rs-spike-down {
  0%   { border-color: #EF5350; box-shadow: 0 0 12px rgba(239,83,80,0.6); }
  33%  { border-color: #FF6B35; box-shadow: 0 0 12px rgba(255,107,53,0.6); }
  66%  { border-color: #E04FC4; box-shadow: 0 0 12px rgba(224,79,196,0.6); }
  100% { border-color: #EF5350; box-shadow: 0 0 12px rgba(239,83,80,0.6); }
}
@media (prefers-reduced-motion: reduce) {
  .wl-item.at-spike-up, .wl-item.at-spike-down { animation: none; }
  /* The 5D pulse now lives on ::before, and stopping it mid-keyframe would
     leave the wash at whatever opacity the default (1) implies — pin it to
     the breath's midpoint so a reduced-motion user still sees the tier. */
  .wl-item.at-high5d::before, .wl-item.at-low5d::before { animation: none; opacity: 0.7; }
}
.wl-item-left { display: flex; align-items: center; gap: 6px; overflow: hidden; flex: 1 1 auto; min-width: 0; }
/* Warm plate behind every sprite. OSRS icons are transparent PNGs drawn for a
   PARCHMENT inventory, so the dark ones — watermelon seeds, coal, black
   equipment, most seeds and logs — are a dark silhouette on a dark panel and
   effectively disappear. Nothing about the row tells you that has happened;
   the icon is simply not there.
   A plate rather than pixel analysis: reading the sprite's own luminance would
   need canvas access to a cross-origin CDN image, which is one CORS header
   away from throwing, and the fallback for a tainted canvas would be this
   anyway. Deterministic beats clever here.
   Low alpha and a warm hue so it reads as the inventory slot it is imitating:
   dark sprites now sit on something, bright ones barely register a change. */
/* No plate here, deliberately — reverted after seeing it on a full list.
   One plate on the hero icon is a fix for a real problem; the same plate on
   every row is 12 of them stacked vertically, which badges the whole list and
   breaks the dense row rhythm the watchlist depends on. Most icons read fine at
   20px; the few that don't are not worth taxing every other row for.
   The hero icon (#tickerHeaderIconLink) and the mobile search sprite keep
   theirs, where there is exactly one and it is the thing being identified. */
.wl-item-left img { width: 21px; height: 21px; image-rendering: pixelated; flex-shrink: 0; }
/* One colour AND one weight for every item name, flagged or not. Names used
   to shift to green / gold / pale-mint depending on why a row was
   highlighted; that went first, because it made the list's most-scanned
   column the least consistent thing in it and put the item's identity in
   the same colour language as its signal.
   Weight was left stepping up on a flagged row, and it turned out to be the
   same mistake one axis over: plain rows at 500 against flagged rows at 800
   is a 300-unit jump, so a six-row list rendered as three bold names and
   three thin ones and looked ragged rather than ranked. The tier is already
   said three times — the coloured accent bar, the breathing glow, and the
   5D badge sitting right after the name. It does not need a fourth voice,
   least of all the one that costs the column its evenness.
   600, not the old 500: with nothing else varying, the names carry the
   scan on their own. */
.wl-item-name { color: #FFFFFF; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; min-width: 0; flex: 0 1 auto; }
.wl-item-left .hl-badge { margin-left: 2px; }
/* Fixed-width, right-aligned data columns so values stack into clean
   columns across every row (TradingView style). Tabular figures keep digits
   the same width. The column-header row uses matching widths. */
/* 26px right gutter mirrors the header's ch-cols margin so data columns line
   up exactly under their labels (the header reserves that space for the ⋯). */
.wl-item-right { display: flex; align-items: center; gap: 5px; text-align: right; flex: 0 0 auto; font-variant-numeric: tabular-nums; }
/* Favorites row management: remove (×) reveals on hover/touch, and — in
   Manual sort only — drag anywhere on a row to reorder (see wireFavoritesDrag).
   Swipe-to-remove on touch works regardless of sort mode. */
.wl-item { position: relative; touch-action: pan-y; }
.wl-edit-btn { background: transparent; border: 1px solid var(--border-light); color: var(--text-muted); border-radius: 3px; font-size: 9px; font-weight: 800; letter-spacing: 0.4px; text-transform: uppercase; padding: 2px 8px; margin-right: 8px; cursor: pointer; transition: all 0.15s ease; }
.wl-edit-btn:hover { color: var(--text-main); border-color: var(--border-active); }
.wl-edit-btn.active { color: var(--accent-ink); background: var(--border-active); border-color: var(--border-active); }
.wl-sort-btn { background: transparent; border: 1px solid var(--border-light); color: var(--text-muted); border-radius: 3px; font-size: 9px; font-weight: 800; letter-spacing: 0.4px; text-transform: uppercase; padding: 2px 7px; margin-right: 8px; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; }
.wl-sort-btn:hover { color: var(--text-main); border-color: var(--border-active); }
.cat-title-right { display: inline-flex; align-items: center; }
/* margin-left:auto on .fls-add does the pushing now; it is no longer inside
   .cat-title-label, so the old 6px nudge has nothing to apply to. */
/* Remove × — pinned to the right edge, hidden by default. There's no longer
   a reserved gutter (columns run to the edge for max name room), so on hover
   the row's data slides left ~22px to open space and the × fades in at the
   right. It therefore still never covers a visible value. On touch we rely
   on swipe-to-remove instead (no static × covering data). */
/* TradingView-style delete: on hover the trash swaps in for JUST the K/M
   suffix of the VOL value — "1.53M" reads "1.53[🗑]". The button sits
   INSIDE the vol cell, absolutely centered over the suffix character
   (.mv-swap wraps both), so it lands on the "M" pixel-exactly at every
   breakpoint. The hidden suffix keeps its layout space so nothing moves. */
.mv-swap { position: relative; display: inline-block; }
.wl-fav-remove {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  background: transparent; border: none; color: var(--text-main);
  width: 16px; height: 16px; cursor: pointer; line-height: 1;
  padding: 0; display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity 0.12s ease, color 0.12s ease;
  z-index: 5;
}
/* Row-gutter fallback trash. Primary trash rides in the VOL cell (over the
   suffix), so the fallback is shown ONLY when the VOL column isn't available
   — either toggled off (container lacks .has-vol) or auto-hidden on a narrow
   sidebar (container query). It still fades in on hover like the in-cell one. */
.wl-fav-remove-row { left: auto; right: 12px; transform: translateY(-50%); display: none; }
#watchlistContent:not(.has-vol) .wl-fav-remove-row { display: flex; }
@container wl (max-width: 264px) { .wl-fav-remove-row { display: flex; } }
.wl-fav-remove svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.wl-fav-remove:hover { color: var(--negative); }
@media (hover: hover) {
  .wl-item[data-fav-row="1"]:hover .wl-fav-remove { opacity: 1; pointer-events: auto; }
  .wl-item[data-fav-row="1"]:hover .meta-vol .mv-sfx { visibility: hidden; }
}
@media (hover: none) {
  .wl-fav-remove { display: none; }
}
.wl-item.swiping { transition: none; }
.wl-item .wl-item-left, .wl-item .wl-item-right { transition: transform 0.18s ease; }
.wl-item.swipe-revealed .wl-item-left,
.wl-item.swipe-revealed .wl-item-right { transform: translateX(-56px); }
.wl-item .wl-swipe-bg { position: absolute; top: 0; right: 0; bottom: 0; width: 56px; display: flex; align-items: center; justify-content: center; background: var(--negative); color: #fff; font-weight: 800; font-size: 11px; letter-spacing: 0.5px; pointer-events: none; opacity: 0; transition: opacity 0.18s ease; }
.wl-item.swipe-revealed .wl-swipe-bg { opacity: 1; }
/* Column widths — ONE source of truth shared by data rows and the header so
   numbers stack into aligned columns. Spacing is baked into the width
   (right-aligned text leaves a natural gap), so no padding mismatches. */
.wl-item-price, .ch-last { width: 40px; text-align: right; flex-shrink: 0; }
.meta-chg, .ch-chg { width: 42px; text-align: right; flex-shrink: 0; }
.meta-ea,  .ch-ea  { width: 30px; text-align: right; flex-shrink: 0; }
.meta-vol, .ch-vol { width: 46px; text-align: right; flex-shrink: 0; }
/* PRIORITY COLUMNS: the item NAME is the one thing that can't survive being
   truncated to "Un…", so when the sidebar gets narrow, drop the low-value
   metric columns before the name gets squeezed — VOL first, then EA, then
   CHG%, keeping LAST price + NAME longest. Container queries key off the
   sidebar's OWN width, so it works for the resizable desktop sidebar and the
   fixed mobile one alike. */
.watchlist-container { container-type: inline-size; container-name: wl; }
@container wl (max-width: 264px) { .meta-vol, .ch-vol { display: none; } }
@container wl (max-width: 226px) { .meta-ea,  .ch-ea  { display: none; } }
@container wl (max-width: 198px) { .meta-chg, .ch-chg { display: none; } }
.wl-item-price { font-weight: 700; font-size: 13px; color: var(--text-main); }
.wl-trend { font-size: 10px; cursor: help; white-space: nowrap; }
.wl-trend.up { color: var(--positive); }
.wl-trend.down { color: var(--negative); }
.wl-trend.neutral { color: var(--text-muted); }
/* Unified meta row — fixed-width right-aligned cells laid out as a flex row
   so each metric stacks into its own column across rows. */
.wl-meta { display: flex; align-items: center; gap: 5px; font-size: 10.5px; line-height: 1.2; white-space: nowrap; font-variant-numeric: tabular-nums; }
.wl-meta .meta-piece { cursor: help; font-weight: 600; }
.wl-meta .meta-chg.up { color: var(--positive); font-weight: 700; }
.wl-meta .meta-chg.down { color: var(--negative); font-weight: 700; }
.wl-meta .meta-chg.neutral { color: var(--text-muted); }
.wl-meta .meta-ea.up { color: var(--positive); font-weight: 700; }
.wl-meta .meta-ea.neutral { color: var(--text-muted); }
/* VOL was text-muted at 9.5px next to the bright red/green CHG% and
   effectively disappeared. Give it real weight in text-main so the eye
   registers it as data on par with change % / margin. */
.wl-meta .meta-vol { color: var(--text-main); font-weight: 700; }
/* Single header bar — ⋯ menu on the LEFT, then the clickable sortable column
   labels. No right gutter: the data columns run to the right edge so the item
   name gets maximum room. Column widths match the data-row cells. */
/* List-name row above the column labels. position:relative because the ⋯
   menu anchors to it now that both live here rather than in .col-header. */
/* The bar's gold stripe, gradient and edge-glow move UP to the host, because
   the host is the full-width row now — the title only spans as far as the ⋯,
   so decorating the title left the menu button sitting outside the bar on
   bare panel. Same treatment .category-title carries, applied one level out
   so it runs the whole width and continues under the ⋯. */
.fav-head-host {
  position: relative; display: flex; align-items: center;
  /* Carries the Favorites header's decoration because .category-title inside
     it is stripped bare (see below), so this has to stay in lockstep with
     the .category-title rule — it is the same bar, drawn one level up. Flat
     panel fill, no bevel, no drop shadow: as a raised gradient chip this was
     the single loudest header in the sidebar, which is what made FAVORITES
     look heavier than its four peers despite identical type. */
  background: var(--bg-panel);
  /* Parchment, not fav-gold. Gold is the app's "you chose this / act on this"
     colour — the saved star, the active timeframe, hover. Spending it on a
     stripe that is permanently there made the loudest accent in the sidebar
     mean nothing in particular. Find Opportunities takes the same parchment,
     so the two section headers read as siblings rather than as two unrelated
     colour codes. */
  /* No side stripe. Every other section keeps its accent, but this one is the
     permanent, always-open list at the top of the sidebar — it does not need a
     colour to tell it apart from anything, and the stripe was the only vertical
     line in the block, which is what made the title and the column names read
     as two stacked bars instead of one. */
  /* No rule underneath: .col-header (ITEM / LAST / CHG% …) sits immediately
     below and draws its own border-top, so this put a second line one row
     above the first. Other sections keep theirs — there the next thing down is
     list rows with no divider of their own. */
  box-shadow: none;
}
.fav-head-host > #favHeadTitle { flex: 1 1 auto; min-width: 0; }
/* 14px, the SAME right inset .col-header uses, so the dots finish on exactly
   the x that VOL does and the block has one right edge instead of two. It was
   6px, which left the dots hanging 8px further right than every column label
   beneath them.
   The + needs no change of its own: it is pushed right by margin-left:auto
   inside #favHeadTitle, which is flex:1 and therefore ends wherever this wrap
   begins — widening this pulls the title's right edge left, and the + rides
   along with it, keeping the gap between the two controls exactly as it was. */
.fav-head-host .ch-menu-wrap { flex: 0 0 auto; display: flex; align-items: center; padding-right: 14px; }
/* The two right-hand controls share one centre line: same 22px box, both
   centred by their flex parents, no stray margins on either. */
.fav-head-host .col-menu { left: auto; right: 6px; }
/* Title keeps only its type and spacing; every decoration is the host's now. */
/* 16px, so FAVORITES lands on the same x as POTENTIAL PROFIT and ANALYST
   RATING. Those sit inside .calc-container's 14px inset; this bar has no such
   parent, so it has to reproduce the inset itself. It was 10px, which put the
   sidebar's five section headers on three different left edges — 1119, 1117
   and 1113 — close enough to look like a mistake rather than a rhythm. */
.fav-head-host .category-title {
  background: transparent; border-left: none; border-bottom: none;
  box-shadow: none; padding-left: 16px;
}
/* Continues the Favorites header rather than sitting under it as its own
   thing. The parchment stripe carries straight down through this row, so the
   title and the column names read as one bar with two lines — which is what
   they are: the second line labels the list the first line names.
   border-top is gone with it. That rule was drawing a divider between two
   halves of the same block, and once the stripe runs through both, a line
   across the middle just contradicts it.
   padding-left drops 6 -> 3 to pay for the 3px border, so the text stays on
   the same left edge it had before the stripe arrived. */
/* padding-left 10px = the same inset .fav-head-host .category-title uses, so
   ITEM starts on exactly the same x as FAVORITES above it. No border on the
   top OR the left: the two rows share one background and one left edge, which
   is what makes them read as a single chunky bar. The bottom border stays —
   that is the boundary between the bar and the list, and it is wanted. */
.col-header { position: relative; display: flex; align-items: center; padding: 5px 14px 6px 16px; font-size: 8.5px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-muted); background: var(--bg-panel); border-bottom: 1px solid var(--border-main); }
.col-header .ch-name { flex: 1 1 auto; min-width: 0; }
.col-header .ch-cols { display: flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
.col-header .ch-sortable { cursor: pointer; border-radius: 3px; transition: color 0.12s ease; }
.col-header .ch-sortable:hover { color: var(--text-main); }
.col-header .ch-sortable.sorted { color: var(--border-active); }
.col-header .ch-arrow { font-size: 8px; margin-left: 1px; }
/* ⋯ column-visibility menu — small button on the left of the header. */
/* Same treatment as .fls-add beside it: no box, colour carries the state.
   It used to grow a filled background and a border on hover, so hovering the
   pair gave you one glyph and one button — two different kinds of control for
   two things that sit together and do the same sort of job.
   22x22 to match .fls-add exactly. It was 20px with a 4px right margin, which
   is what put the dots off the plus's centre line: a 2px height difference
   centres 1px high, and the margin pushed it off horizontally too.
   Gold on hover AND while the menu is open — aria-expanded is already
   maintained for accessibility, so the open state is free to style from it
   rather than needing a second class kept in sync. */
.col-menu-btn {
  flex-shrink: 0;
  background: transparent; border: 0; color: var(--text-structural);
  border-radius: 4px; font-size: 15px; line-height: 1;
  width: 22px; height: 22px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 0.12s ease;
}
/* Optical nudge, and it needs one even though the BOXES are already level.
   align-items:center centres the line box, not the ink inside it, and "⋯"
   draws its dots below the middle of its own line box while "+" sits on the
   centre. Measured, the two 22px boxes share a centre line exactly — the
   glyphs still did not, which is why matching the box sizes alone did not
   fix it. This aligns what you actually see. */
.col-menu-btn { transform: translateY(-2px); }
.col-menu-btn:hover, .col-menu-btn:focus-visible,
.col-menu-btn[aria-expanded="true"] {
  color: var(--fav-gold, #FFB300); background: transparent; border: 0;
}
.col-menu {
  display: none; position: absolute; left: 6px; top: calc(100% + 2px); z-index: 60;
  min-width: 200px; padding: 8px;
  background: var(--bg-panel); border: 1px solid var(--border-light);
  border-radius: 6px; box-shadow: 0 10px 30px rgba(0,0,0,0.55);
  text-transform: none; letter-spacing: 0; font-weight: 400;
}
.col-menu.open { display: block; }
.col-menu-section { font-size: 9px; font-weight: 800; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-muted); padding: 4px 6px 6px; }
.col-menu-row { display: flex; align-items: center; gap: 8px; padding: 6px 6px; font-size: 12px; color: var(--text-main); cursor: pointer; border-radius: 4px; }
.col-menu-row:hover { background: var(--bg-input); }
.col-menu-sep { height: 1px; background: var(--border-main); margin: 5px 2px; }
/* Destructive, so it is the one row in this menu that is not neutral — but
   quiet until it is armed, because a permanently red button in a settings
   menu reads as an error state rather than an action. */
.col-menu-danger {
  display: block; width: 100%; text-align: left;
  padding: 6px; border: none; border-radius: 4px; background: transparent;
  font-family: inherit; font-size: 12px; font-weight: 600;
  color: var(--text-muted); cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.col-menu-danger:hover { background: rgba(239,83,80,0.12); color: var(--negative); }
.col-menu-danger.is-armed { background: rgba(239,83,80,0.16); color: var(--negative); font-weight: 800; }
.col-menu-row input { margin: 0; accent-color: var(--border-active); cursor: pointer; }
/* Each row leads with the exact header token that column shows, because the
   descriptive names didn't connect: "Margin (after tax)" and the "EA" column
   read as two different things, so the switch for EA looked missing. */
.col-menu-tag { display: inline-block; min-width: 30px; margin-right: 6px; font-size: 9px; font-weight: 800; letter-spacing: .4px; text-transform: uppercase; color: var(--border-active); font-variant-numeric: tabular-nums; }

/* Width alone, not orientation — a narrow LANDSCAPE window (mouse or
   touch) can't fit the full desktop header/target-boxes any better than a
   narrow portrait phone can; reusing this compact layout (instead of
   inventing a second one) is what actually fixes that instead of leaving
   narrow-landscape windows with no compact treatment at all. Genuinely
   short+narrow TOUCH devices in landscape still get the separate,
   further-compacted single-row treatment below (gated on pointer: coarse,
   not width), since that's a different, more cramped case (~390px tall)
   than this block assumes. */
@media screen and (max-width: 640px) {
  /* Portrait: the new 30-day meter + summary run edge-to-edge otherwise;
     give them a little horizontal breathing room like the rest of content. */
  .item-insight { padding-left: 12px; padding-right: 12px; margin-left: 0; margin-right: 0; }
  /* Mobile portrait: every gap above the chart is collapsed so the chart
     starts as high up the screen as possible. Earlier this section saved
     wrap / row-wrapper space; the price strip was still eating ~60px
     because it stacked label / value / grade vertically. Below it now
     renders single-row (label-on-left, value, then grade-chip right), like
     the desktop layout but smaller. Saves ~30px above the chart. */
  /* Override the desktop "height: 100dvh" so .wrap can grow tall enough
     to fit the stacked-column mobile layout. Without this, content (chart
     + watchlist + below-the-fold sections) overflowed the fixed-height
     wrap, and even after unfixing the body the in-wrap scroll trapped
     the user inside .wrap — they could never scroll past it to reach the
     footer / about section / FAQ. Now .wrap sizes naturally; the body
     scrolls the whole document. */
  .wrap { padding: 4px; gap: 4px; height: auto; min-height: 100dvh; overflow-y: visible; }
  .main-column { gap: 3px; }
  .top-row-wrapper { gap: 2px; }
  /* overflow:hidden on the top panel is a guard against icons clipping past
     the right edge of the viewport — if the math is off on a future device,
     the row will scroll-clip cleanly instead of running off the screen. */
  /* No overflow:hidden here — the ⋯ overflow menu is an absolutely-
     positioned dropdown that must be allowed to spill below the panel. */
  /* Horizontal padding to zero. This panel has no card, border or background
     of its own, so the 6px was invisible except as an indent: it pushed the
     row's first control 6px right of the item rows, Target boxes and pill row
     that all start at the content edge, and pulled the search field's right
     edge 6px in from the same. The menu button reads as "weirdly to the right"
     because of it — measured, its glyph started at x=21 while the star on the
     row below starts at 12. */
  .top-panel { padding: 3px 0; position: relative; }
  .panel-left { width: 100%; align-items: center; flex-wrap: nowrap; justify-content: space-between; gap: 4px; min-width: 0; }
  /* Keep just the scimitar icon — the "PocketGE" wordmark is what doesn't
     fit next to search + icons at this width, but a tap-to-refresh/home
     affordance is still worth the ~20px it costs.
     Size stays at the base 18px. It was briefly taken to 24 to match the
     #itemIcon sprite beside it — the row's other controls were all short
     against a 36px search field — and 24 came back visibly soft. Unlike the
     item icons, which are hard-edged inventory sprites from
     static.runelite.net, this one is a wiki PNG whose edges are already
     anti-aliased; 18 is where that softness stays under the pixel grid.
     Making the logo bigger AND crisp means sourcing it from the same cache
     the item icons use, not scaling this file further. */
  #logoBtn { margin-right: 0; flex-shrink: 0; }
  #logoBtn img { margin-right: 0; }
  .brand-logo .brand-word { display: none; }
  /* Matches the search field it sits beside. It was 26px against a 30px bar,
     which read as near enough; against the 36px bar it reads as undersized.
     align-self:flex-start went with it — measured, that pinned the pill's
     centre 5px above every other control in the row, so it was both smaller
     AND not on the same line. Default alignment centres it with the rest. */
  .mode-pill { flex-shrink: 0; height: 36px; padding: 2px; }
  .mode-pill button { padding: 0 7px; font-size: 11px; }
  /* Cramming mode-pill + search + 5 icons into one row was overflowing on
     ~390px iPhones (the "?" button kept clipping off the right edge).
     Wrap inside search-group: search input takes a full row, the 5 buttons
     drop to a second row underneath. Both rows now have breathing room and
     nothing clips. */
  /* Single tidy row: search input grows, then the ⋯ overflow menu. The
     secondary actions (Alerts/Bank/Help) collapse into the ⋯ dropdown so
     the row stops looking cluttered. Favorite lives in the ticker header
     below, so it's not in this row anymore. */
  .search-group { flex: 1 1 0; min-width: 0; gap: 5px; flex-wrap: nowrap; align-items: center; }
  .sbar { flex: 1 1 auto; min-width: 0; width: auto; }
  /* font-size MUST be >= 16px on touch inputs or iOS Safari auto-zooms in
     when the user taps to type, distorting the entire layout. Keep 16px. */
  /* 30 -> 36px, and the sprite 16 -> 24px inside it. On desktop the selected
     item is stated twice — the 48px chip beside the name AND this sprite — but
     portrait hides the whole name block to save a row, so this one 16px sprite
     was carrying the entire "which item am I looking at" job. It was the
     smallest thing on screen doing the most important work.
     The 6px of height goes to the sprite, not to padding, so the bar reads as
     an item chip with a field attached rather than just a taller box. */
  .sbar input { padding: 3px 6px 3px 34px; height: 36px; font-size: 16px; }
  #itemIcon {
    left: 5px; width: 24px; height: 24px; padding: 2px;
    box-sizing: border-box; border-radius: 4px;
    background: rgba(255,255,255,0.06);
    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
  }
  .icon-btn { flex: 0 0 auto; padding: 5px 9px; font-size: 15px; gap: 0; }
  /* Watchlist rows: 10px of vertical padding down to 6px. Small per row, but
     this list is the longest thing on the page — across the rows on screen it
     is worth roughly a whole extra row of list. Only the padding moves: the
     20px sprite, the type size and the tap target are all left alone, so rows
     stay comfortably thumb-sized. */
  .wl-item { padding: 6px 12px; }
  /* Compact fav button in the ticker header on portrait mobile. */
  .fav-btn { padding: 5px 9px; gap: 5px; flex: 0 0 auto; font-size: 11px; }
  .fav-btn .fav-star { width: 13px; height: 13px; }
  /* ⋯ overflow menu on mobile: button shows, container becomes a dropdown */
  .more-wrap { display: inline-flex; position: relative; flex: 0 0 auto; }
  /* 38px wide -> 34, and the height pinned to 36 so it fills the same band as
     the search field rather than floating 3px short at each end. The width
     comes off because with the panel's indent gone, 10px of horizontal padding
     was the remaining thing holding the glyph away from the content edge.
     Losing 4px of width is paid back by the taller box: 38x30 = 1140, 34x36 =
     1224, so the tap target grows. */
  .more-toggle { display: inline-flex !important; font-size: 18px; line-height: 1; padding: 5px 7px; min-width: 34px; min-height: 36px; justify-content: center; }
  .more-actions .icon-btn { width: 100%; justify-content: flex-start; gap: 10px; padding: 9px 10px; font-size: 15px; border-color: transparent; background: transparent; }
  .more-actions .icon-btn:hover, .more-actions .icon-btn:active { background: var(--bg-input); }
  .more-actions .icon-label { display: inline; font-size: 13px; }
  .content-area { flex-direction: column; gap: 3px; }
  /* Portrait stacking order. Everything below the chart collapses into one
     column, and the default DOM order buried the watchlist under the range
     meter AND the Recommended Flip — roughly a full screen of scrolling
     before the list you actually came back for.
     Reordered to: the verdict on the item you're looking at (Potential
     Profit + Analyst Rating), then your other items, then the supporting
     detail, then discovery. Navigation you use constantly outranks reference
     you consult occasionally.
     #itemInsight is a direct child here because placeItemInsight() moves it
     into this container in portrait, so plain `order` is enough — no second
     relocation needed. */
  .watchlist-container > .mode-pill-side { order: 0; }
  .watchlist-container > #sideModule { order: 1; }
  .watchlist-container > #favHeadHost { order: 2; }
  .watchlist-container > #colHeader { order: 3; }
  .watchlist-container > #watchlistContent { order: 4; }
  .watchlist-container > #itemInsight { order: 5; }
  .watchlist-container > #flipFinder { order: 6; }
  .watchlist-container > #findOppsHost { order: 7; }
  /* Combined search overlay: the current item name overlays the search box
     (so it also acts as a display of the loaded item), with a ↗ wiki chip
     at the right. Same pattern as landscape — the ticker-title-group's
     separate name row is entirely eliminated, saving a whole row for the
     chart. Save button + timeframe share the reclaimed row instead. */
  /* Left inset follows the sprite chip (5 + 24 + 5), so the name starts clear
     of it rather than overlapping — this is the one number that has to move
     whenever the icon size does. */
  .sbar-item {
    display: flex; position: absolute; inset: 1px 24px 1px 34px; align-items: center;
    pointer-events: none; background: var(--bg-input); color: var(--text-main);
    font-weight: 700; font-size: 15px; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; border-radius: 4px;
  }
  .sbar:focus-within .sbar-item { display: none; }
  .sbar-wiki {
    display: flex; position: absolute; right: 3px; top: 50%; transform: translateY(-50%);
    width: 22px; height: 22px; align-items: center; justify-content: center;
    z-index: 3; pointer-events: auto; color: var(--text-muted);
    font-size: 12px; font-weight: 700; text-decoration: none;
    border: 1px solid var(--border-light); border-radius: 4px; background: var(--bg-panel);
  }
  .sbar-wiki:hover, .sbar-wiki:active { color: #fff; border-color: var(--border-active); }
  .sbar input { padding-right: 32px; }
  /* Ticker-title-group is now redundant — item name lives in the sbar. */
  .ticker-title-group { display: none !important; }
  .ticker-header { padding: 0; border: none; background: transparent; gap: 8px; row-gap: 0; height: auto; flex-direction: row; flex-wrap: nowrap; align-items: center; }
  /* Save button leads the timeframe row so it's the first thing after the
     search bar — easy thumb tap, and the timeframes get the rest of the row.
     Icon-only (star + gold already signal "favorite") so there's enough
     width left for the 6-button toolbar to share this same row instead of
     wrapping onto its own line below with the star stranded alone. */
  /* 30 -> 26px, and the timeframe pills are given the same floor. The star was
     the tallest thing in this row and therefore what set its height, while the
     pills beside it sat at 21px — a 9px mismatch that read as the star being
     oversized AND cost the row 5px it was not using. 26 keeps a comfortable
     touch target while letting the row close up around its contents. */
  /* box-sizing so min-height is the OUTER height. Without it the star's 1px
     border sits outside the 26px and it renders 28 — still 2px taller than the
     pills, which is the mismatch this was meant to remove. */
  .ticker-header .fav-btn { padding: 3px 6px; gap: 0; flex: 0 0 auto; min-height: 22px; box-sizing: border-box; }
  .ticker-header .fav-btn .fav-star { width: 16px; height: 16px; }
  .ticker-header .fav-btn .fav-label, .ticker-header .fav-btn .fav-label-active { display: none; }
  /* ── Mobile Save button ──────────────────────────────────────────────
     One container, two star states. The container is identical whether or
     not the item is saved — a 10% gold wash and a clean 1px #E5B842 hairline
     — so toggling never makes a box appear, grow or light up. The ONLY
     thing that changes is the star: hollow and muted when unsaved, solid
     warm gold when saved.
     No box-shadow in any state. An outer glow is the language of a primary
     action; a favourite toggle is a passive indicator, and at phone size a
     12px bloom on a 30px button is most of the button.
     #E5B842 rather than --fav-gold (#FFB300): the amber is the loudest gold
     on the page and it is doing real work in the watchlist's 5D badges. The
     warm gold is the same one Target Buy uses, which is the right weight for
     a control sitting in the header.
     NOTE: this rule is duplicated across the two mobile breakpoints below
     (narrow-portrait and short-landscape). They drifted apart once already —
     one kept a 12px glow the other had lost — so change them together. */
  .ticker-header .fav-btn {
    background: transparent; border: 1px solid var(--border-main);
    color: var(--text-muted); box-shadow: none;
  }
  .ticker-header .fav-btn:hover { border-color: var(--border-active); color: #fff; box-shadow: none; }
  /* Gold only once SAVED, matching the desktop button and the Refresh
     control beside it. The container used to be gold in both states, which
     answered the flashbang but left nothing to mark the state with except
     the star's fill — and spent the accent on an item nobody had saved. */
  .ticker-header .fav-btn.active {
    background: rgba(229, 184, 66, 0.10);
    border-color: #E5B842; color: #E5B842; box-shadow: none;
  }
  .toolbar { flex: 1 1 auto; min-width: 0; justify-content: space-between; padding: 2px; align-items: center; }
  .toolbar button { flex: 1; padding: 5px 2px; font-size: 12px; min-width: 0; line-height: 1; }
  .pct-chg { display: none !important; }
  /* svh, NOT vh. On iOS `vh` resolves against the LARGE viewport — the height
     the page would have with the URL bar and toolbar hidden — so 52vh is
     around 69% of what is actually on screen while that chrome is showing.
     Combined with the header and target boxes above it, the chart ran off the
     bottom of the visible area on first load. `svh` is the SMALL viewport,
     i.e. chrome visible, so this is 52% of what you can really see, and it
     does not resize as the URL bar hides (dvh would, which re-renders the
     canvas every time you scroll). vh first as the fallback for anything
     without svh. Capped in px too so a large phone doesn't get an absurd one.
     min-height 320 -> 260: on a small phone 320 was itself most of the screen,
     which is the same bug with a different number. */
  /* 52 -> 42svh. At 52 the chart plus the header and target boxes above it
     filled the visible area, so the first thing below — the insta-buy strip,
     then Potential Profit, then the watchlist — was entirely off screen on
     load, and the page opened looking like a chart with nothing under it.
     42svh keeps the chart the largest single element while letting what
     follows it show above the fold, so there is a visible reason to scroll.
     min-height 260 -> 230 for the same reason on the smallest phones, where a
     percentage is not what is binding. The drag handle still reaches the old
     size for anyone who wants it. */
  .chart-container { flex: none; height: 42vh; height: 42svh; min-height: 230px; max-height: 460px; min-width: 0; margin-bottom: 0; }
  .watchlist-container { width: 100%; flex: 1; min-height: 400px; }
  /* Target Buy + Target Sell SIDE BY SIDE on one line (was two stacked
     full-width rows — double the height for the same two numbers). Each box:
     tiny label stack on the left, big value, grade number on the right.
     The verdict words ("WAIT FOR DIP") don't fit half a phone's width, so
     the colored grade number carries the verdict — its color already maps
     to buy/sell strength, and the desktop/landscape views keep the words. */
  .price-strip { flex-direction: row; gap: 4px; }
  .price-strip .price-box { flex: 1 1 0; width: auto; min-width: 0; }
  .price-box { display: flex; align-items: center; gap: 6px; padding: 3px 8px; min-width: 0; overflow: hidden; }
  /* The number outranks its own caption. These were the other way round —
     label flex-shrink:0, value flex-shrink:1 — so in a 189px box the caption
     ("Insta-sell: 1,181,199,999 · just now") claimed 124px and left the
     target 39px, which is why a Scythe rendered as "1,18". No font size
     fixes that; the space was already spent. The caption shrinks and
     ellipsises now, the value never does. */
  .price-label { flex-direction: column; align-items: flex-start; gap: 0; flex-shrink: 1; min-width: 0; font-size: 8px; }
  .live-val { font-size: 8px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .price-val { font-size: 17px; margin-top: 0; margin-left: auto; flex-shrink: 0; white-space: nowrap; line-height: 1.1; letter-spacing: -0.4px; }
  .calc-container input { font-size: 16px; }
}
/* (pointer: coarse) keeps this "mobile landscape" restructure — logo
   hidden, item name folded into the search box, everything squeezed onto
   one row — scoped to actual touch devices. height+orientation alone also
   matches a short, wide DESKTOP window, which wrongly got the full phone
   treatment just because the window was short. A narrow-but-not-touch
   desktop window still needs SOME help so things don't overlap, but it
   gets a much lighter, non-restructuring fix — see the plain
   ".price-strip .price-box" width rule further down, which is keyed on
   width alone and doesn't touch the logo/search/layout at all. */
@media screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  /* Mobile landscape: vertical space is the scarce axis (~390px tall) while
     width is abundant (~844px). So DON'T stack — put the top panel (search +
     icons) and the ticker (name + timeframe) side by side on ONE row, and
     Reclaims ~80px straight into the chart,
     which was being squeezed to ~100px. */
  .wrap { padding: 3px; gap: 3px; }
  /* ONLY once the chart has been hand-dragged. The fixed 100dvh is what makes
     the chart's `flex: 1` fill exactly one screen here, so relaxing it
     unconditionally has two costs I measured before scoping it: the default
     layout grew ~44px past the fold, and — worse — a reset stopped working,
     because with the column's height indefinite `flex: 1` means "content
     height" and the chart simply kept whatever the canvas bitmap already was
     (548px, unchanged by the reset that had correctly cleared --chart-h).
     Dragged, the chart has an explicit height and nothing needs to stretch,
     so the wrap can grow and push the quick-facts row and the 30-Day Range
     below the fold — which is the whole point of the gesture. Double-click
     removes .user-sized, this rule stops matching, and `flex: 1` refills one
     screen exactly as before. Same shape as the desktop rule. */
  .wrap:has(.chart-container.user-sized) { height: auto; min-height: 100dvh; }
  .main-column { gap: 3px; }
  .content-area { flex-direction: row; gap: 4px; }
  .top-row-wrapper { flex-direction: row; gap: 4px; height: auto; align-items: stretch; }
  /* Horizontal padding to zero — same reasoning as portrait: no card here, so
     it only indented the first control away from the edge everything below it
     starts at. */
  .top-panel { flex: 1 1 55%; min-width: 0; padding: 3px 0; }
  .panel-left { flex-wrap: nowrap; gap: 4px; }
  /* 30 -> 36, matching the search field it sits beside. Free vertically: the
     panel is already 36px tall because the search bar makes it so, and the
     pill was just sitting 3px short at each end inside that. */
  .mode-pill { height: 36px; flex: 0 0 auto; }
  /* Trims a few px off the F2P/P2P pill so the item name next to it has
     room to spell out longer names ("Emerald necklace") instead of
     truncating — see .ticker-header split below for the other half. */
  .mode-pill button { padding: 0 6px; }
  /* Keep just the scimitar icon — see the matching rule in the compact
     breakpoint above, including why its size stays at the base 18px. */
  #logoBtn { margin-right: 0; flex-shrink: 0; }
  #logoBtn img { margin-right: 0; }
  .brand-logo .brand-word { display: none; }
  /* Combined search: the current item name overlays the search box (which is
     otherwise a squished, near-invisible sliver here), and a ↗ opens its
     wiki. The item is therefore shown ONCE — the separate ticker name is
     hidden below. Focusing the field hides the name so you can type. */
  .search-group { flex: 1 1 auto; min-width: 0; }
  .sbar { flex: 1 1 auto; min-width: 90px; width: auto; }
  /* Same 36px bar and 24px sprite as portrait. Landscape is pinned to 100dvh
     so the instinct is to keep this row as short as possible, but measured it
     makes no difference at all: the chart comes out at 296px whether this bar
     is 30px or 36px, because the row's height is set by the logo and mode pill
     beside it, not by the search field. (A first pass here shaved the bar back
     on the strength of a 10px chart difference that turned out to be the
     activity bar landing between the two measurements, not this rule.) */
  .sbar input { padding: 2px 26px 2px 34px; height: 36px; font-size: 16px; }
  #itemIcon {
    left: 5px; width: 24px; height: 24px; padding: 2px;
    box-sizing: border-box; border-radius: 4px;
    background: rgba(255,255,255,0.06);
    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
  }
  .sbar-item { display: flex; position: absolute; inset: 1px 24px 1px 34px; align-items: center;
    pointer-events: none; background: var(--bg-input); color: var(--text-main); font-weight: 700;
    font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-radius: 4px; }
  .sbar:focus-within .sbar-item { display: none; }
  .sbar-wiki { display: flex; position: absolute; right: 3px; top: 50%; transform: translateY(-50%);
    width: 21px; height: 21px; align-items: center; justify-content: center; z-index: 3; pointer-events: auto;
    color: var(--text-muted); font-size: 12px; font-weight: 700; text-decoration: none;
    border: 1px solid var(--border-light); border-radius: 4px; background: var(--bg-panel); }
  .sbar-wiki:hover, .sbar-wiki:active { color: #fff; border-color: var(--border-active); }
  /* Collapse the 4 secondary actions into the ⋯ dropdown (like portrait) so
     the row isn't cluttered and the combined search has room. */
  .more-wrap { display: inline-flex; position: relative; flex: 0 0 auto; }
  /* min-height 30 -> 36 for the same reason as .mode-pill above: the row is
     already 36 tall, and 30 just left the button short at both ends. */
  .more-toggle { display: inline-flex !important; padding: 4px 6px; min-width: 32px; min-height: 36px; justify-content: center; }
  .more-actions .icon-btn { width: 100%; justify-content: flex-start; gap: 10px; padding: 9px 10px; font-size: 15px; border-color: transparent; background: transparent; }
  .more-actions .icon-label { display: inline; font-size: 13px; }
  /* Ticker header carries the Save button + timeframe toolbar (item name
     lives in the search box above). */
  .ticker-title-group { display: none !important; }
  .ticker-header { flex: 1 1 45%; min-width: 0; padding: 2px; border: none; background: transparent; display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; }
  /* Icon-only Save button in landscape — the star + gold color already
     signal "favorite", and the "Save"/"Saved" label doesn't fit next to a
     6-button toolbar in a ~470 px header. Tap area stays big enough. */
  /* 30 -> 26px, and the timeframe pills are given the same floor. The star was
     the tallest thing in this row and therefore what set its height, while the
     pills beside it sat at 21px — a 9px mismatch that read as the star being
     oversized AND cost the row 5px it was not using. 26 keeps a comfortable
     touch target while letting the row close up around its contents. */
  /* box-sizing so min-height is the OUTER height. Without it the star's 1px
     border sits outside the 26px and it renders 28 — still 2px taller than the
     pills, which is the mismatch this was meant to remove. */
  .ticker-header .fav-btn { padding: 3px 6px; gap: 0; flex: 0 0 auto; min-height: 22px; box-sizing: border-box; }
  .ticker-header .fav-btn .fav-star { width: 17px; height: 17px; }
  .ticker-header .fav-btn .fav-label, .ticker-header .fav-btn .fav-label-active { display: none; }
  /* ── Mobile Save button ──────────────────────────────────────────────
     One container, two star states. The container is identical whether or
     not the item is saved — a 10% gold wash and a clean 1px #E5B842 hairline
     — so toggling never makes a box appear, grow or light up. The ONLY
     thing that changes is the star: hollow and muted when unsaved, solid
     warm gold when saved.
     No box-shadow in any state. An outer glow is the language of a primary
     action; a favourite toggle is a passive indicator, and at phone size a
     12px bloom on a 30px button is most of the button.
     #E5B842 rather than --fav-gold (#FFB300): the amber is the loudest gold
     on the page and it is doing real work in the watchlist's 5D badges. The
     warm gold is the same one Target Buy uses, which is the right weight for
     a control sitting in the header.
     NOTE: this rule is duplicated across the two mobile breakpoints below
     (narrow-portrait and short-landscape). They drifted apart once already —
     one kept a 12px glow the other had lost — so change them together. */
  .ticker-header .fav-btn {
    background: transparent; border: 1px solid var(--border-main);
    color: var(--text-muted); box-shadow: none;
  }
  .ticker-header .fav-btn:hover { border-color: var(--border-active); color: #fff; box-shadow: none; }
  /* Gold only once SAVED, matching the desktop button and the Refresh
     control beside it. The container used to be gold in both states, which
     answered the flashbang but left nothing to mark the state with except
     the star's fill — and spent the accent on an item nobody had saved. */
  .ticker-header .fav-btn.active {
    background: rgba(229, 184, 66, 0.10);
    border-color: #E5B842; color: #E5B842; box-shadow: none;
  }
  .toolbar { flex: 1 1 auto; width: auto; min-width: 0; padding: 0; gap: 5px; align-items: center; }
  /* line-height:1 so each label's box is the glyph itself. Inherited
     leading otherwise pads the text box unevenly against the 30px
     neighbours, which reads as the row sitting low. */
  .toolbar button { flex: 1; min-width: 0; padding: 4px 0; font-size: 13px; min-height: 30px; line-height: 1; }
  .pct-chg { display: none !important; }
  .chart-container { min-height: 0; }
  /* Sidebar in landscape: the recommended-flip card + Potential-Profit
     module used to eat the whole 230px-tall column, leaving the favorites
     list clipped to nothing (it's flex:1 with overflow hidden on the
     container). Let the WHOLE sidebar scroll as one unit instead, so the
     flip card, calculator, then every watchlist row are all reachable. */
  /* Viewport-based cap, not the base `max-height: 100%`. Once a dragged chart
     makes .wrap's height indefinite (see the :has rule above) a percentage
     resolves to `none`, and a long favourites list would grow this column to
     its full content height instead of scrolling inside it. Same trap as the
     desktop cap and the splitter's. */
  .watchlist-container { width: 234px; font-size: 11px; overflow-y: auto; -webkit-overflow-scrolling: touch; max-height: calc(100dvh - 6px); }
  /* Favorites is the reason this column exists, but it sat behind ~320px of
     modules in a viewport only ~390-515px tall, so it opened essentially
     below the fold. The container is a flex column, so `order` re-stacks it
     for this breakpoint with no JS and no markup change.
     Recommended Flip drops to last: expanded it's the single biggest block
     here (~200px vs ~60px collapsed), and unlike the modules above it it
     describes a DIFFERENT item — it's a discovery prompt, so it belongs
     with Find Opportunities at the bottom rather than above the list.
     It also defaults to collapsed at this breakpoint, and the preference is
     now stored per layout (see collapseKey), so collapsing here no longer
     follows you to desktop — but order is still what reliably keeps it out
     of the way regardless of collapse state.
     #sideModule stays on top deliberately: Potential Profit + Analyst
     Rating are compact (~120px together) and they're the verdict on the
     chart being viewed right now. Pushing them below a long favorites list
     would mean never seeing them, which is a worse trade than the space
     they cost. colHeader has to travel with watchlistContent — it's that
     list's column header, not a standalone block. */
  .watchlist-container > .mode-pill-side { order: 0; }
  .watchlist-container > #sideModule { order: 1; }
  .watchlist-container > #favHeadHost { order: 2; }
  .watchlist-container > #colHeader { order: 3; }
  .watchlist-container > #watchlistContent { order: 4; }
  .watchlist-container > #flipFinder { order: 5; }
  .watchlist-container > #findOppsHost { order: 6; }
  /* Fixed compact width here (not --wl-width), so the drag handle has
     nothing meaningful to resize — keep it hidden in this cramped view. */
  .wl-resize { display: none; }
  .watchlist-content { flex: 0 0 auto; overflow: visible; }
  .wl-item { padding: 4px 6px; font-size: 11px; }
  .calc-container { padding: 4px 6px; gap: 3px; }
  .calc-container input { padding: 4px 6px; font-size: 16px; }
  /* Target boxes were stacked (label / value / grade on three rows) in
     landscape — that's what made them tall. Lay each box out on a SINGLE row
     like portrait (label · value · grade), with a tiny grade chip and no
     "1D"/"?" context. Cuts the strip height roughly in half. */
  .price-strip { gap: 4px; }
  .price-box { display: flex; align-items: center; gap: 8px; padding: 2px 8px; min-width: 0; overflow: hidden; }
  .price-label { flex-direction: column; align-items: flex-start; gap: 0; flex-shrink: 0; font-size: 9px; }
  .live-val { font-size: 9px; }
  .price-val { font-size: 15px; margin-top: 0; margin-left: auto; flex-shrink: 0; white-space: nowrap; line-height: 1.1; letter-spacing: -0.3px; }
  .price-step { width: 20px; height: 20px; font-size: 13px; border-radius: 5px; }
  /* NOTE: the compact recommended-flip-card overrides for landscape live in
     their own media block AFTER the .flip-card base styles (search
     "landscape flip-card compaction") — placing them here would lose to the
     later base rules, since media queries don't add specificity. */
}

/* Inline UI icons (emoji replacements) — sized to sit on the text line. */
.ui-ic { width: 14px; height: 14px; flex: 0 0 auto; vertical-align: -2px; }
.ui-ic.ic-spin { animation: uiIcSpin 1.2s linear infinite; }
@keyframes uiIcSpin { to { transform: rotate(360deg); } }
.icon-btn .ui-ic { width: 15px; height: 15px; display: block; }
/* The hamburger's ink, on the layouts where it shares a row with a 36px search
   field and a 24px item sprite. At 15px it was the smallest mark in that row
   by some way, which is the other half of "not as visually tall as search" —
   the button's box is transparent, so the glyph IS the button as far as the
   eye is concerned. 20px puts it in the same weight class as the scimitar
   beside it without touching the desktop row, where 15 is right.
   ID selector on purpose: `.icon-btn .ui-ic` above is same-specificity and
   later in source than the mobile blocks, so a class rule there would lose. */
@media screen and (max-width: 640px),
       screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  #btnMore .ui-ic { width: 20px; height: 20px; }
}
.cat-title-label { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.fc-kicker { display: inline-flex; align-items: center; gap: 5px; }
.find-flip-btn .ff-bolt .ui-ic { width: 16px; height: 16px; display: block; }
.lf-emoji .ui-ic { width: 22px; height: 22px; color: var(--border-active); }

/* Tiny "?" affordance on the grade chips → opens the glossary. */
.pg-help {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; margin-left: 6px; flex-shrink: 0;
  border-radius: 50%; border: 1px solid var(--text-muted);
  color: var(--text-muted); font-size: 9px; font-weight: 700;
  cursor: help; line-height: 1;
}
.pg-help:hover { color: #fff; border-color: #fff; }

/* Chart loading skeleton — shown until the first real draw lands. */
.chart-loading {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px;
  color: var(--text-muted); font-size: 13px; pointer-events: none;
  background: var(--bg-chart); z-index: 3;
}
.chart-loading.hidden { display: none; }
.chart-loading .cl-spinner {
  width: 26px; height: 26px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,0.12); border-top-color: var(--border-active);
  animation: cl-spin 0.8s linear infinite;
}
@keyframes cl-spin { to { transform: rotate(360deg); } }



/* Site footer — trust / attribution signal below the about content. */
.site-footer {
  max-width: 880px; margin: 0 auto 48px; padding: 18px 20px 0;
  border-top: 1px solid var(--border-main);
  color: var(--text-muted); font-size: 11.5px; line-height: 1.6;
}
.site-footer .foot-row { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; margin-bottom: 8px; }
.site-footer .foot-brand { color: var(--text-main); font-weight: 800; }
.site-footer .foot-version { color: var(--text-muted); font-size: 10.5px; padding: 1px 7px; border: 1px solid var(--border-light); border-radius: 4px; letter-spacing: 0.4px; font-weight: 700; }
.site-footer .foot-fresh { color: var(--positive); }
.site-footer .foot-updated { color: var(--text-muted); }
/* The top bar carried a second copy of the footer's deploy stamp — a
   rounded "Updated today" pill beside the logo. It was removed, and the
   reason is worth keeping: sitting inches from live price readings that say
   "just now" and "5m ago", a coarse day-grained label reads as a claim
   about the PRICES rather than about the site's code. It is the vaguest
   timestamp on a page whose whole job is precise ones, and vagueness there
   costs trust rather than building it — a flipper who thinks the quote
   might be fourteen hours old will not act on it.
   Nothing was lost: the footer stamp is the same element with the same
   class, and wireBuildInfoCard binds the deploy card to every
   .js-build-updated, so the exact timestamp, SHA and commit link are still
   one click away down there. */
/* Deploy-detail card behind the "Updated …" stamp (see wireBuildInfoCard).
   Both stamps get a pointer cursor so the rounded label reads as openable. */
.js-build-updated { cursor: help; }
.build-info-pop {
  position: fixed; z-index: 1200; width: 264px; padding: 11px 12px;
  background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.65); text-align: left; color: var(--text-main);
  opacity: 0; visibility: hidden; transform: translateY(-4px); pointer-events: none;
  transition: opacity .14s ease, visibility .14s, transform .14s ease;
}
.build-info-pop.open { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; }
.build-info-pop .bi-eyebrow { font-size: 9px; font-weight: 800; letter-spacing: .7px; text-transform: uppercase; color: var(--text-muted); }
.build-info-pop .bi-when { font-size: 13px; font-weight: 800; margin-top: 3px; }
.build-info-pop .bi-sub { font-size: 10px; color: var(--text-muted); margin-top: 1px; }
.build-info-pop .bi-sha { display: inline-block; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border-main); width: 100%; font-size: 10px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--border-active); text-decoration: none; }
.build-info-pop .bi-sha:hover { text-decoration: underline; }
.build-info-pop .bi-by { margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--border-main); font-size: 10.5px; }
.build-info-pop .bi-by-k { color: var(--text-muted); margin-right: 5px; }
.build-info-pop .bi-by b { color: var(--fav-gold); font-weight: 800; }
.build-info-pop .bi-by-n { display: block; color: var(--text-muted); font-size: 9.5px; margin-top: 2px; line-height: 1.45; }

/* ── Share: hover hint + share sheet ─────────────────────────────────────── */
/* The hint names the item and lists what actually ends up in the card, so
   there's no guessing before the click. It was load-bearing when Share sat
   in the Analyst Rating header, where the placement itself said "share the
   rating"; beside the chart's own controls the button no longer misleads,
   but naming what a one-click publish is about to publish is worth keeping
   on its own terms. Same shell as .build-info-pop so the two hovers feel
   like one thing. */
.share-tip {
  position: fixed; z-index: 1200; width: 250px; padding: 10px 12px;
  background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.65); text-align: left; color: var(--text-main);
  opacity: 0; visibility: hidden; transform: translateY(-4px); pointer-events: none;
  transition: opacity .14s ease, visibility .14s, transform .14s ease;
}
.share-tip.open { opacity: 1; visibility: visible; transform: translateY(0); }
.share-tip .st-title { font-size: 12.5px; font-weight: 800; line-height: 1.35; }
.share-tip .st-title b { color: var(--fav-gold); font-weight: 800; }
.share-tip .st-sub { font-size: 10px; color: var(--text-muted); margin-top: 5px; line-height: 1.5; }
.share-tip .st-list { margin: 8px 0 0; padding: 8px 0 0; border-top: 1px solid var(--border-main); list-style: none; }
.share-tip .st-list li { font-size: 10.5px; color: var(--text-main); line-height: 1.6; padding-left: 13px; position: relative; }
.share-tip .st-list li::before { content: '·'; position: absolute; left: 4px; color: var(--border-active); font-weight: 800; }

/* Our own share sheet, shown instead of the OS one on pointer devices — see
   the comment on shareCard(). Card width is driven by the preview, which is
   the whole point of the dialog: you see the exact PNG before it goes out. */
.share-sheet .modal-card { max-width: 520px; }
.share-preview { border: 1px solid var(--border-main); border-radius: 6px; overflow: hidden; background: #0A0908; line-height: 0; }
/* The card is tall (roughly 1200x990), so on a short laptop an unbounded
   preview pushes the action tiles below the fold — and the tiles are the
   reason the dialog exists. Cap it against the viewport and letterbox
   rather than crop, so the preview stays an honest picture of the PNG. */
.share-preview img { width: 100%; height: auto; max-height: min(360px, 38vh); object-fit: contain; display: block; }
.share-caption {
  margin-top: 10px; padding: 8px 10px; border: 1px solid var(--border-main); border-radius: 6px;
  background: var(--bg-base); color: var(--text-muted); font-size: 10.5px; line-height: 1.55;
  white-space: pre-wrap; max-height: 74px; overflow-y: auto;
}
.share-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 12px; }
.share-act {
  display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 11px 5px;
  background: var(--bg-base); border: 1px solid var(--border-main); border-radius: 7px;
  color: var(--text-main); font-size: 11px; font-weight: 700; text-align: center;
  cursor: pointer; text-decoration: none; transition: border-color .12s ease, background .12s ease;
}
.share-act:hover { background: var(--bg-input); border-color: var(--border-active); text-decoration: none; color: #fff; }
.share-act svg { width: 19px; height: 19px; flex: 0 0 19px; }
.share-act .sa-sub { font-size: 9px; font-weight: 600; color: var(--text-muted); line-height: 1.3; }
.share-act.done { border-color: var(--positive); color: var(--positive); }
.share-act.done .sa-sub { color: var(--positive); }
.share-step {
  margin-top: 11px; padding: 9px 11px; border-radius: 7px;
  background: rgba(201,166,77,0.10); border: 1px solid var(--border-active);
  color: var(--text-main); font-size: 11px; line-height: 1.55;
}
.share-step b { color: var(--border-active); font-weight: 800; }
.share-step .ss-title { font-size: 12px; font-weight: 800; color: #fff; margin-bottom: 6px; }
.share-step .ss-steps { margin: 0; padding-left: 18px; }
.share-step .ss-steps li { margin-bottom: 3px; }
.share-step .ss-ok { color: var(--positive); }
.share-step .ss-go {
  display: inline-flex; align-items: center; margin-top: 9px; padding: 7px 14px;
  border-radius: 6px; border: 1px solid var(--border-active); background: var(--border-active);
  color: var(--accent-ink); font: 800 11.5px/1 inherit; cursor: pointer;
}
.share-step .ss-go:hover { filter: brightness(1.08); }
.share-step .ss-why { margin-top: 8px; font-size: 9.5px; color: var(--text-muted); line-height: 1.45; }
.share-step kbd {
  font-family: inherit; font-size: 10px; font-weight: 700; padding: 1px 4px;
  border: 1px solid var(--border-light); border-radius: 3px; background: var(--bg-base);
}
.share-foot { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--border-main); display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.share-foot .sf-note { font-size: 9.5px; color: var(--text-muted); line-height: 1.45; }
.share-foot .sf-native { flex: 0 0 auto; background: transparent; border: none; padding: 0; cursor: pointer; font-size: 10.5px; font-weight: 700; color: var(--border-active); }
.share-foot .sf-native:hover { text-decoration: underline; }
@media (max-width: 480px) { .share-grid { grid-template-columns: repeat(2, 1fr); } }

.site-footer .foot-cta { color: var(--border-active); font-weight: 700; }
.site-footer .foot-sources { font-size: 10.5px; opacity: 0.8; }
.site-footer a { color: var(--border-active); text-decoration: none; }
.site-footer a:hover { text-decoration: underline; }
.site-footer .foot-disclaimer { opacity: 0.7; font-size: 10.5px; }

/* ── Recommended-flip finder ─────────────────────────────────────────────── */
/* Same section treatment as .calc-container (Potential Profit + Analyst
   Rating): full-bleed panel, one bottom rule, matching padding. Recommended
   Flip used to be a rounded, shadowed card floating inside a padded wrapper,
   which read as a widget parked inside the sidebar rather than a section of
   it — a nested box among flat siblings. */
.flip-finder { background: var(--bg-panel); border-bottom: 1px solid var(--border-main); padding: 12px 14px; }
.find-flip-btn { width:100%; display:flex; align-items:center; justify-content:center; gap:8px;
  padding:12px; border:none; border-radius:10px; cursor:pointer; font-weight:800; font-size:14px;
  color:#1a1200; background:linear-gradient(135deg,var(--buy-color) 0%,#FFD24D 100%);
  box-shadow:0 2px 10px rgba(255,184,0,.25); }
.find-flip-btn:active { transform:translateY(1px); }
.find-flip-btn .ff-bolt { font-size:16px; }
.flip-card { margin:0; padding:0; cursor:pointer; background:transparent; border:none; box-shadow:none; }
.flip-card:focus-visible { outline:2px solid var(--buy-color); outline-offset:2px; }
/* Internal rhythm matched to the sections above. Measured, these gaps were
   6/7/6px against Analyst Rating's 10px, so the section read tighter than
   its peers even after it stopped being a nested card — four stacked bands
   (name+score, edge line, buy/sell, stats) with barely a hairline between
   them. */
.fc-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:10px; }
.fc-kicker { font-size:var(--hdr-size); font-weight:var(--hdr-weight); letter-spacing:var(--hdr-track); color:var(--text-structural); text-transform:uppercase; }
.fc-head-ctrls { display:flex; align-items:center; gap:6px; flex:0 0 auto; }
/* Same quiet control as Refresh and Save: transparent, hairline, muted,
   brightening on hover. It was a filled chip, which made the one button in
   this card that only cycles a suggestion look heavier than the buy/sell
   prices underneath it. */
.fc-next { min-height:34px; font-size:11.5px; font-weight:700; padding:6px 12px; border-radius:999px; cursor:pointer;
  color:var(--text-muted); background:transparent; border:1px solid var(--border-main);
  transition:color .15s ease, border-color .15s ease, transform .12s ease; }
.fc-next:hover:not(:disabled) { color:#fff; border-color:var(--border-active); }
.fc-next:active { transform:translateY(1px); }
.fc-next:disabled { opacity:.5; cursor:default; }
/* Collapse toggle + collapsed one-line summary. Collapsing hides everything
   in .fc-collapsible and reveals .fc-mini (name · score · edge).
   The button wears .calc-caret, so it IS the same component as the Analyst
   Rating and Potential Profit expanders — same 22px gold chip, same 13px
   glyph, and the icon rotates via .closed rather than the button rotating
   itself. It was a 30px white-bordered square with a 14px glyph, which
   matched nothing else on the page. */
.fc-collapse { flex:0 0 auto; border:none; padding:0; cursor:pointer; }

.fc-mini { display:none; align-items:center; gap:8px; min-width:0; }
.fc-mini-icon { width:22px; height:22px; flex:0 0 22px; image-rendering:pixelated; }
/* Pushed RIGHT, with the controls column reserved beneath it.
   This has been both ways. Pinned right with nothing reserved, the score sat
   directly under the Next button and the two read as one cluster; the fix
   was to pack it left, which then parked it a few px from the item name and
   made a phone read "Steel bar SCORE 83" as one run-on string while the
   whole right half of the row sat empty.
   Neither end is the problem — the collision is. margin-left:auto pushes the
   score to the end of the row, and the 92px padding on the collapsed row
   (see below) stops "the end of the row" from meaning "under Next". The
   name still shrinks first (0 1 auto + ellipsis) rather than shoving the
   score anywhere. */
.fc-mini-name { flex:0 1 auto; min-width:0; font-weight:800; font-size:13px; color:var(--text-main); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.fc-mini-score { flex:0 0 auto; margin-left:auto; font-weight:900; font-size:14px; line-height:1; }
/* A bare number needed a noun, and "SCORE" was a poor one — it named the
   field instead of saying anything, on the one line where space is scarcest.
   The verdict word says what the number means, so it replaces the label and
   follows the figure: "83 STRONG". It inherits the verdict colour rather
   than sitting muted, because the word and the number are now one reading
   rather than a label and its value. "Flip" is trimmed off upstream — the
   header six pixels above already says Recommended flip. */
.fc-mini-score-k { font-size:9px; font-weight:800; letter-spacing:.4px; text-transform:uppercase; color:inherit; opacity:.85; }
.flip-card.collapsed { padding:0; }
.flip-card.collapsed .fc-head { margin-bottom:0; }
.flip-card.collapsed .fc-collapsible { display:none; }
/* The 92px right reservation that used to live here is GONE. It existed to
   keep the score out from under the Next button — correct when the score was
   packed left and could drift, and obsolete since the score was pinned right
   with margin-left:auto. Measured at eight widths (740/844/932 landscape,
   360/390 portrait, 1024/1440/1920 desktop) the header row and the mini row
   are on separate lines in every one, and Next never overlaps the mini row
   in any of them, so the reservation was protecting against a collision that
   cannot occur.
   It was not free. In landscape the mini row is 220px wide, so 92px of it —
   42% — was blank space held for nothing, and the item name ellipsised down
   to a single letter to pay for it. That is the bug: the name was not being
   covered by anything, it was being squeezed by a gap. */
/* 10px, matching the expanded card's .fc-head margin-bottom and the Analyst
   Rating block above it. An earlier pass lifted this section's internal
   rhythm from 6px to 10px to stop it reading tighter than its peers, but
   only the EXPANDED bands were moved — collapsed kept overriding back to 6,
   which is why the score still sat almost directly under the Next button
   with nothing between them. The horizontal fix for that pair landed a
   while ago (the score packs left, out from under Next); this is the
   vertical half of the same problem. */
.flip-card.collapsed .fc-mini { display:flex; margin-top:10px; padding-right:2px; }
/* The name and the score are two separate readings, not one phrase, so the
   score gets more room than the 8px that ties the icon to the name it
   belongs to. Same reason the kicker steps away from its own number. */
/* PADDING, not margin. This rule outranks the base `.fc-mini-score` on
   specificity, so setting margin-left here would overwrite the `auto` that
   does the pushing and silently drop the score back beside the name —
   which is the bug this change exists to fix. */
.flip-card.collapsed .fc-mini-score { padding-left:10px; }
/* The word trails the number now, so the gap moves to the other side. */
.flip-card.collapsed .fc-mini-score-k { margin-left:4px; }
.flip-card.collapsed .fc-mini-icon { width:18px; height:18px; flex-basis:18px; }
.flip-card.collapsed .fc-mini-name { font-size:12px; }
.flip-card.collapsed .fc-mini-score { font-size:13px; }
/* Secondary while collapsed: no fill, quieter text, tighter box — so Next
   reads as a control beside the label rather than a second headline
   competing with the score below it. Lights up on hover/focus. */
/* Collapsed only changes the SIZE. It used to restate the colours too, with
   its own white-ish border and text — and since collapsed is the default
   state now, that override was the version everyone actually saw, which is
   why Next was the one control that hovered differently from Refresh and
   Save sitting inches away. */
.flip-card.collapsed .fc-next {
  font-size:10.5px; min-height:26px; padding:4px 10px;
}
/* Keep "Next" available while collapsed so recs can be cycled without
   expanding. Header layout: the kicker takes what's left after Next +
   chevron; the label ellipsizes as a last-resort safety net rather than
   being hidden.
   BOTH states, not just collapsed. This was scoped to .collapsed, so an
   expanded card let "Recommended Flip" wrap to two lines in the narrow
   landscape sidebar — reported from an iPhone. It does not reproduce in
   Chromium at 740, 844 or 932 landscape, where the label measures 127px and
   fits: it is the font-metric slack the .fc-collapse note below is already
   worried about, and a device whose glyphs run a few px wider tips it over.
   Which is the argument for pinning it rather than trusting the fit — same
   reasoning as .calc-headline-label, whose comment says exactly this about
   "Potential Profit". Wrapping costs a whole line of a section that is meant
   to be one; ellipsizing costs a couple of characters of a name you can
   already infer. */
.fc-kicker { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Collapsed used to drop the label to 9.5px, which made this section name
   visibly smaller than its four peers in the state users see most. It fits
   at full size next to Next + chevron; the ellipsis above is the safety net
   if a narrower container ever proves otherwise. */
.fc-body { display:flex; align-items:center; gap:8px; }
.fc-icon { width:30px; height:30px; flex:0 0 30px; image-rendering:pixelated; }
.fc-main { flex:1 1 auto; min-width:0; }
.fc-name { font-weight:800; font-size:14px; color:var(--text-main); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.fc-why { font-size:10.5px; color:var(--text-muted); margin-top:3px; line-height:1.45; }
.fc-gauge { flex:0 0 auto; text-align:center; min-width:58px; position:relative; }
.fc-score { font-size:18px; font-weight:800; line-height:1; }
.fc-verdict { font-size:9.5px; font-weight:700; margin-top:1px; }
.fc-meter { height:3px; border-radius:2px; background:rgba(255,255,255,.10); margin-top:3px; overflow:hidden; }
.fc-meter span { display:block; height:100%; border-radius:2px; }
/* Score explainer. The score is the one figure on the card with no unit and
   no label, so it invites "91 out of what?". A generic definition wouldn't
   answer that — two picks can sit points apart for completely different
   reasons — so this shows THIS flip's own point breakdown, plus the band
   ladder so the number has a visible scale. Trigger is a real <button>:
   hover on a mouse, tap on touch, and reachable from the keyboard. */
.fc-score-btn { display:block; width:100%; background:none; border:none; padding:0; margin:0; font:inherit; color:inherit; cursor:help; text-align:center; }
.fc-score-btn:focus-visible { outline:2px solid var(--border-active); outline-offset:2px; border-radius:4px; }
/* Fixed, not absolute, and positioned in JS. The card lives inside the
   scrolling watchlist sidebar, which clips its overflow — in compact landscape
   that container is barely taller than this popup, so an absolutely-positioned
   one was measured getting cut off entirely. Fixed escapes every clipping
   ancestor; the trade is that it has to be placed (and dismissed on scroll)
   by hand. Stays a DOM child of .fc-gauge so mouseleave still ignores the
   pointer travelling from the number into the popup. */
.fc-tip {
  position:fixed; z-index:1200; width:230px;
  background:var(--bg-panel); border:1px solid var(--border-light); border-radius:8px;
  box-shadow:0 12px 32px rgba(0,0,0,.65); padding:10px; text-align:left;
  opacity:0; visibility:hidden; transform:translateY(-4px); pointer-events:none;
  transition:opacity .14s ease, visibility .14s, transform .14s ease;
}
.fc-tip.open { opacity:1; visibility:visible; transform:translateY(0); pointer-events:auto; }
.fc-tip-h { font-size:11.5px; font-weight:800; letter-spacing:.2px; }
.fc-tip-sub { font-size:10px; color:var(--text-muted); line-height:1.45; margin:2px 0 8px; }
.fc-tip-row { display:flex; align-items:baseline; justify-content:space-between; gap:8px; font-size:10.5px; color:var(--text-main); padding:2.5px 0; }
.fc-tip-row span:first-child { color:var(--text-muted); }
.fc-tip-row b { font-weight:800; font-variant-numeric:tabular-nums; flex:0 0 auto; }
.fc-tip-row.tot { border-top:1px solid var(--border-main); margin-top:5px; padding-top:6px; font-size:11.5px; }
.fc-tip-row.tot span:first-child { color:var(--text-main); font-weight:800; }
.fc-tip-cap { font-size:9px; color:var(--fav-gold); margin:-1px 0 4px; }
.fc-tip-bands { display:flex; gap:3px; margin-top:9px; }
.fc-tip-band { flex:1 1 0; text-align:center; font-size:8px; font-weight:800; text-transform:uppercase; letter-spacing:.2px; padding:3px 1px; border-radius:4px; background:var(--bg-input); color:var(--text-muted); }
.fc-tip-band.on { color:#0C0B09; }
.fc-tip-foot { font-size:9.5px; color:var(--text-muted); line-height:1.45; margin-top:8px; }
.fc-prices { display:flex; gap:8px; margin:11px 0 10px; }
/* Buttons, not labels — tapping one moves the chart's target line the same
   way the Target Buy / Target Sell boxes do, so they carry the same selected
   treatment (white rim + tinted glow) as those boxes. */
.fc-price { flex:1; padding:5px 6px; border-radius:8px; text-align:center; font:inherit; color:inherit; cursor:pointer; transition:border-color .15s ease, box-shadow .15s ease; }
.fc-price.buy  { background:rgba(229, 184, 66,.10); border:1px solid rgba(229, 184, 66,.30); }
.fc-price.sell { background:rgba(38, 169, 171,.10); border:1px solid rgba(38, 169, 171,.30); }
.fc-price.buy:hover  { border-color:rgba(229, 184, 66,.65); }
.fc-price.sell:hover { border-color:rgba(38, 169, 171,.65); }
.fc-price.selected { border-color:#fff; }
.fc-price.buy.selected  { box-shadow:0 0 0 1px rgba(255,184,0,.25), 0 4px 12px rgba(255,184,0,.10); }
.fc-price.sell.selected { box-shadow:0 0 0 1px rgba(38, 169, 171,.30), 0 4px 12px rgba(38, 169, 171,.12); }
.fc-price:focus-visible { outline:2px solid var(--border-active); outline-offset:2px; }
.fc-plabel { display:block; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.3px; }
.fc-price.buy .fc-plabel { color:var(--buy-color); }
.fc-price.sell .fc-plabel { color:var(--sell-color); }
.fc-pval { display:block; font-size:15px; font-weight:800; color:var(--text-main); margin-top:1px; }
.fc-stats { display:flex; flex-wrap:wrap; gap:4px 12px; }
.fc-stats > div { display:flex; flex-direction:column; flex:1 1 auto; }
.fc-slabel { font-size:9px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.3px; }
.fc-sval { font-size:12.5px; font-weight:700; color:var(--text-main); font-variant-numeric:tabular-nums; }
.fc-sval.pos { color:var(--rs-green-deep); }
.fc-subnote { font-size:9px; color:var(--text-muted); }
.flip-card.ff-msg { display:flex; flex-direction:column; align-items:center; gap:10px; text-align:center; cursor:default; padding:6px 0; }
.ff-msg-txt { font-size:12px; color:var(--text-muted); }

/* landscape flip-card compaction — MUST live here (after the .flip-card base
   rules) so it wins on source order; media queries add no specificity. The
   short landscape sidebar can't afford the full card, so shrink it and put
   Profit / Est-per-4h / Daily-vol on ONE inline line. (pointer: coarse)
   scopes this to real touch devices — see the matching block above. */
@media screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  /* ── Collapsed Analyst Rating: verdict and meter share a row ───────────
     Measured on a 844x390 handset, the collapsed gauge was 74px of a 390px
     viewport. The verdict word alone accounted for 23 of that (17px line +
     6px margin) sitting on its own row above an 18px meter that says the
     same thing a second way.
     The fix is not to delete the word. The word is the unambiguous half —
     "Strong Buy" needs no decoding, whereas a needle three-fifths along an
     unlabelled bar needs the reader to know the scale. Dropping it would
     leave the louder, vaguer element and call that an improvement. They go
     on ONE row instead: word at its natural width, meter taking the rest.
     Nothing is lost and the row count halves.
     Grid rather than flex because .rg-top has to stay full-width above
     them, and grid can say that in one line. */
  .rg-wrap.is-collapsed {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    align-items: center; column-gap: 10px;
    /* row-gap, because without it the two rows TOUCH — measured, the meter's
       top sat 2px ABOVE the controls' bottom, so the bar physically overlapped
       Share and the chevron rather than sitting under them. The meter also
       spans the full column width, which puts its right end directly beneath
       both controls; portrait and desktop do the same thing harmlessly because
       their stacked layout leaves 21-25px of vertical air. This grid had none.
       8px is what buys that air back. It costs 8 of the 25px this layout
       saved, which is the right trade: the saving was for the watchlist, not
       for letting a bar run into a button. */
    row-gap: 8px;
  }
  .rg-wrap.is-collapsed .rg-top { grid-column: 1 / -1; }
  .rg-wrap.is-collapsed .rg-label { margin-bottom: 0; font-size: 13px; }
  /* Some padding stays: the needle's pointer is an ::after at bottom:-6px
     living in this padding, so zeroing it would clip the one part of the
     meter that says where the price actually sits. */
  .rg-wrap.is-collapsed .rg-meter { padding-bottom: 7px; }

  /* ── Favorites header ──────────────────────────────────────────────────
     It measured 35px against 28px data rows — the label was taller than the
     things it labels, on the axis where this layout has least to give. The
     padding does it: 6px top and bottom around a 22px switcher button. */
  .fav-head-host .category-title { padding-top: 3px; padding-bottom: 3px; }
  .fav-head-host .fls-current .fls-caret .ui-ic { width: 13px; height: 13px; }
  .flip-finder { padding: 4px 6px; }
  .flip-card { margin: 0; padding: 0; }
  .flip-card .fc-why { display: none; }
  .flip-card .fc-head { margin-bottom: 4px; }
  .flip-card .fc-icon { width: 24px; height: 24px; flex-basis: 24px; }
  .flip-card .fc-name { font-size: 12px; }
  .flip-card .fc-gauge { min-width: 50px; }
  .flip-card .fc-score { font-size: 15px; }
  .flip-card .fc-next { min-height: 26px; padding: 4px 9px; font-size: 11px; }
  /* Collapsed header in the narrow landscape sidebar: shrink Next and the
     chevron so the kicker keeps its full 11px alongside them. Measured at
     844x390 — the label fits without ellipsizing, so it no longer drops to
     9px and stops being the odd one out among the five section headers. */
  .flip-card.collapsed .fc-next { min-height: 22px; padding: 3px 5px; font-size: 10px; }
  .flip-card.collapsed .fc-head-ctrls { gap: 3px; }
  .flip-card.collapsed .fc-head { gap: 4px; }
  .flip-card.collapsed .fc-kicker { gap: 4px; }
  /* Pure chrome, and the cheapest px to buy: the collapse chevron's 22px
     chip is decoration around a 13px glyph, so trimming it to 18px leaves
     the label ~5px of slack for font metrics that differ from Chromium's
     rather than sitting at an exact fit and ellipsizing on someone's
     device. The chip is still a comfortable target next to Next. */
  .flip-card.collapsed .fc-collapse { width: 18px; height: 18px; }
  /* "Potential Profit" is the longest of the five section names and clears
     this 234px sidebar by ~2px, so pin it to one line rather than let a
     future tweak silently wrap it while its four peers stay single-line. */
  .calc-headline-label { white-space: nowrap; }
  .flip-card .fc-prices { margin: 4px 0; gap: 4px; }
  .flip-card .fc-price { padding: 3px 5px; }
  .flip-card .fc-pval { font-size: 13px; }
  .flip-card .fc-plabel { font-size: 9px; }
  .flip-card .fc-stats { flex-wrap: nowrap; gap: 8px; }
  .flip-card .fc-stats > div { flex-direction: row; align-items: baseline; gap: 3px; min-width: 0; flex: 0 1 auto; }
  .flip-card .fc-slabel { font-size: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .flip-card .fc-sval { font-size: 11px; white-space: nowrap; }
}

/* ── Analyst Rating gauge ────────────────────────────────────────────────── */
.rg-wrap { margin:8px 0 2px; padding:9px 12px 8px; background:var(--bg-panel); border:1px solid var(--border-hair); border-radius:8px; }
.rg-wrap[hidden] { display:none; }
.rg-top { display:flex; align-items:center; gap:6px; margin-bottom:3px; }
.rg-eyebrow {
  display:inline-flex; align-items:center; gap:5px;
  font-size:var(--hdr-size); font-weight:var(--hdr-weight); letter-spacing:var(--hdr-track);
  text-transform:uppercase; color:var(--text-structural);
}
.rg-top .pg-help { margin-left:0; }
/* Share button (see placeShareButton) — icon-only, in one of two hosts
   depending on width. Hidden by default and switched on per host: its markup
   home is the ticker header next to the favourite star, which is also where
   the wide placement puts it, so the default-off keeps the narrow layouts
   from flashing it beside Save for the frame before placeShareButton() runs.
   Bare glyph, no chip, in both. Everywhere else .fav-btn is a labelled
   control that needs a boundary to read as pressable; here it is a lone icon,
   and a border around it makes a box that is mostly empty — and next to the
   timeframe pills a bordered chip would read as a seventh timeframe, which is
   exactly what the segmented group beside it is made of. The padding stays so
   the tap target does not shrink to the glyph: the box goes, the target does
   not. Colour carries the affordance instead — muted at rest, white on
   hover/focus, the same as .ct-btn and .qf-refresh next to it. */
#btnShare { display:none; }
.ticker-header #btnShare, .qf-row #btnShare, .panel-left #btnShare { display:inline-flex; }
#btnShare.fav-btn {
  padding:3px 5px; gap:0; flex:0 0 auto; min-height:0;
  border:0; background:transparent; color:var(--text-muted);
}
#btnShare.fav-btn:hover, #btnShare.fav-btn:focus-visible { color:#fff; background:transparent; border:0; }
#btnShare .fav-label { display:none; }
#btnShare svg { width:15px; height:15px; }
/* Pills to the second row, Share to the end of the first. Only set when
   placeShareButton has measured that the name row can take it — see there. */
.ticker-header.hdr-share-inline .toolbar { order: 1; }
/* Wide: parked at the far right of the header, not tucked in behind 5Y.
   An 18px gap was enough to keep this from reading as a seventh timeframe,
   but "not quite touching the pills" still leaves it in their orbit — the
   eye takes the whole run from 1D to the glyph as one busy strip. An auto
   margin puts the row's empty space BETWEEN the options and the action,
   which is where it does some work: the pills end, nothing, then a single
   control against the sidebar edge.
   The toolbar is `flex: 0 1 auto` with its own overflow-x, so on a narrow
   window it gives up the slack first and scrolls, exactly as it did when
   the space after it was empty. */
.ticker-header #btnShare { margin-left:auto; }
/* Short header: beside the search box instead. .search-group is `flex: 1 1
   auto`, so this lands hard against the right edge of that row with no
   margin of its own needed — the search field gives up the width. 36px to
   sit on the same centre line as the 36px search input rather than floating
   against its top edge. */
.panel-left #btnShare { height:36px; flex:0 0 auto; }
/* Narrow: the quick-facts row, sharing order:3 with Refresh so it falls in
   right after it on source order alone — no renumbering of the four
   `order`s this row already depends on. 26px to match .ct-btn and
   .qf-refresh either side of it. */
.qf-row #btnShare { order:3; height:26px; padding:0 6px; }
/* Collapsed: eyebrow + Buy/grade/Sell scale drop away, leaving the verdict
   and the bar. .rg-top is lifted out of the flow and pinned top-right so the
   toggle sits on the SAME line as the verdict rather than holding a row of
   its own — collapsing that row is most of the height saving, so leaving it
   in place would defeat the point. */
.rg-wrap { position:relative; }
/* Wears .calc-caret so this expander is literally the same component as the
   Potential Profit one — size, chip, chevron and the .closed rotation all
   come from there, so the two can't drift apart. Only the <button> resets
   and the placement are specific to this one.
   order:3 puts it AFTER Share (which carries margin-left:auto), so every
   expander on the page ends up hard against the right edge. */
/* 18px. Share and this chevron are the only two controls on the row, they do
   completely different things — one publishes, one folds a panel — and at
   the original 6px they read as a single two-part widget. 10px was a first
   pass and still too tight to separate them; with the flex gap that comes to
   a 24px measured gap, which is wider than either control, so the eye stops
   grouping them.
   Set on the shared rule rather than a landscape override: it is the same
   pair at every width. Landscape is where the tightness SHOWS, not where it
   differs, and per-width values here are what produced three chevrons that
   behaved differently before. */
/* margin-left:auto, not 18px. The 18px separated this chevron from the Share
   button beside it, and Share carried the auto margin that pushed the pair
   to the right edge. Share has moved to the chart, so this is now the only
   thing after the eyebrow and has to do its own pushing. */
.rg-toggle { border:none; padding:0; cursor:pointer; order:3; margin-left:auto; }
/* Hover/press come from .calc-caret above — this used to redeclare its own
   and was the reason the three chevrons behaved differently. */
/* Collapsing hides the SUPPORTING detail — the BUY/SELL scale row and the
   four-row breakdown — and nothing else. The header row stays exactly where
   it is, so "ANALYST RATING" is on screen in both states and the block is
   never an unlabelled verdict floating above a bar.
   This used to yank .rg-top out of flow and pin it top-right, which hid the
   eyebrow and the help "?" as a side effect and pulled the meter up under
   the buttons — where, on a Strong Buy/Sell with the needle pinned to an
   end, the needle's 2px overhang collided with them. Keeping the row in
   flow removes the collision along with the workarounds it needed (trimmed
   buttons, a 2px right offset, extra margin under the verdict). */
.rg-wrap.is-collapsed .rg-scale { display:none; }
.rg-wrap.is-collapsed .rg-why { display:none; }
.rg-label { font-size:15px; font-weight:900; letter-spacing:-.2px; line-height:1.1; margin-bottom:6px; }
.rg-label.is-strong { text-shadow:0 0 14px currentColor; }
.rg-meter { position:relative; padding-bottom:9px; }
.rg-track { display:flex; gap:2px; height:9px; border-radius:5px; overflow:hidden; }
.rg-seg { flex:1; transition:filter .2s ease, opacity .2s ease; }
.rg-ss { background:rgba(38, 169, 171,.34); } .rg-s { background:rgba(38, 169, 171,.17); }
.rg-n  { background:rgba(120,123,134,.16); }
.rg-b  { background:rgba(229, 184, 66,.17); } .rg-sb { background:rgba(229, 184, 66,.34); }
.rg-seg.is-live { filter:brightness(1.9) saturate(1.3); box-shadow:inset 0 0 0 1px rgba(255,255,255,.28); }
/* Dim the inactive segments so the gauge reads as a SCALE with one live
   position (needle + lit segment), not a progress bar filling toward the
   other side. The colored halves stay: gold = buy half, teal = sell half,
   mirroring the Target Buy / Target Sell boxes. */
.rg-meter:not(.is-muted) .rg-seg:not(.is-live) { opacity: 0.55; }
.rg-meter.is-muted .rg-seg { opacity:.4; filter:grayscale(.5); }
.rg-needle { position:absolute; top:-2px; width:3px; height:13px; border-radius:2px; background:var(--rg-hue,var(--text-muted));
  transform:translateX(-50%); box-shadow:0 0 8px var(--rg-hue,transparent); transition:left .35s cubic-bezier(.25,.8,.35,1); }
.rg-needle::after { content:""; position:absolute; left:50%; bottom:-6px; transform:translateX(-50%);
  border-left:4px solid transparent; border-right:4px solid transparent; border-top:5px solid var(--rg-hue,var(--text-muted)); }
.rg-meter.is-muted .rg-needle { opacity:.55; box-shadow:none; }
.rg-scale { display:flex; align-items:baseline; justify-content:space-between; gap:8px; margin-top:4px; font-size:9px; font-weight:700; letter-spacing:.3px; }
.rg-end { text-transform:uppercase; opacity:.75; } .rg-end-sell { color:var(--sell-color); } .rg-end-buy { color:var(--buy-color); }
.rg-sub { flex:1; text-align:center; font-size:9.5px; font-weight:600; color:var(--text-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Score breakdown — expanded only. Deliberately plain: rows of
   name / plain-English reason / points, so it reads as the arithmetic
   behind the word rather than a second chart to interpret. */
.rg-why { margin-top:10px; padding-top:9px; border-top:1px solid var(--border-main); }
.rg-why-head { display:flex; align-items:baseline; justify-content:space-between; gap:8px; margin-bottom:7px; }
.rg-why .rw-t { font-size:9px; font-weight:800; letter-spacing:.5px; text-transform:uppercase; color:var(--text-muted); }
.rg-why .rw-sides { display:inline-flex; align-items:baseline; gap:5px; font-size:10.5px; font-weight:800; }
.rg-why .rw-sides i { font-style:normal; font-weight:600; color:var(--text-muted); font-size:9px; }
.rg-why .rw-buy { color:var(--buy-color); }
.rg-why .rw-sell { color:var(--sell-color); }
.rg-why-row { display:grid; grid-template-columns:52px 1fr 42px 46px; align-items:center; gap:6px; margin-bottom:4px; }
.rg-why .rw-k { font-size:9.5px; font-weight:800; color:var(--text-main); }
.rg-why .rw-v { font-size:9.5px; color:var(--text-muted); line-height:1.35; }
.rg-why .rw-bar { height:4px; border-radius:2px; background:rgba(255,255,255,0.07); overflow:hidden; }
.rg-why .rw-bar i { display:block; height:100%; background:var(--border-active); }
.rg-why .rw-p { font-size:9.5px; font-weight:800; color:var(--text-main); text-align:right; font-variant-numeric:tabular-nums; }
.rg-why .rw-p em { font-style:normal; font-weight:600; color:var(--text-muted); }
.rg-why-foot { margin-top:7px; font-size:9px; color:var(--text-muted); line-height:1.5; }
/* Market Read — deliberately the quietest thing in the card. It sits below the
   verdict line behind a hairline, in muted type, with the eyebrow at the same
   weight as the other section labels but no colour of its own. It must read as
   context for the rating, never as a second, competing call. */
/* A left rule rather than a hairline above, and NEUTRAL text. The presence
   comes from structure, not colour, and that's deliberate: this block fires in
   both directions — "heavy volume near the low" and "heavy volume near the
   high, watch for rejection" come out of the same engine — so any directional
   colour would be wrong half the time. Gold in particular is the app's buy /
   you-chose-this accent, so gold body text would read as a buy signal on a
   caption that also appears when an item is topping out, and would give the
   one line that ends "not a prediction" the same weight as the numbers you act
   on. The rule is gold because it's a bracket, not a verdict.
   6px of indent, not 10: at 10 this caption wrapped to a third line, and the
   height is worth more to the chart on a phone than the extra breathing room
   is worth here. */
.rg-read { margin-top:9px; padding-left:6px; border-left:2px solid var(--border-active); }
.rg-read .rr-t { display:block; font-size:8.5px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--text-muted); }
.rg-read .rr-b { margin:3px 0 0; font-size:11px; line-height:1.5; color:#B9B1A3; }
@media (min-width:1024px){ .rg-read .rr-b { font-size:11.5px; } }
/* No collapsed-state rule needed: this lives inside .rg-why, which the
   collapsed state already hides. */
/* The reason text is the first thing to wrap on a narrow column, so give it
   the room and let the bar go. Keyed off the SIDEBAR's width, not the
   viewport's: coarse landscape is an 844px-wide window with a 234px column,
   so a viewport media query missed exactly the layout that needed this most
   and left "0% up the 1D range (2,440-2,500)" wrapping to three lines
   beside a decorative bar. */
@container wl (max-width: 300px) {
  .rg-why-row { grid-template-columns:50px 1fr 44px; }
  .rg-why .rw-bar { display:none; }
}
@media (max-width: 640px) {
  .rg-why-row { grid-template-columns:50px 1fr 44px; }
  .rg-why .rw-bar { display:none; }
}
@media (min-width:1024px){ .rg-wrap{padding:10px 14px;} .rg-label{font-size:16px;} .rg-track{height:10px;} .rg-sub{font-size:10px;} }
/* Sidebar variant: lives inside the Potential Profit module (.calc-container is
   already a panel) — divider instead of its own box. */
.rg-wrap.rg-side { margin:8px 0 0; padding:8px 2px 0; background:transparent; border:none; border-top:1px solid var(--border-main); border-radius:0; }
@media (min-width:1024px){ .rg-wrap.rg-side{padding:8px 2px 0;} }

/* ── Quick-facts (below chart) ───────────────────────────────────────────── */
.quick-facts { display:flex; align-items:center; gap:8px; flex-wrap:nowrap; overflow-x:auto;
  -webkit-overflow-scrolling:touch; scrollbar-width:none; padding:5px 2px; margin:3px 0 0; font-size:11px; color:var(--text-muted); }
.quick-facts::-webkit-scrollbar { display:none; }
.qf-cell { display:inline-flex; align-items:baseline; gap:4px; white-space:nowrap; flex:0 0 auto; }
/* A tooltip nobody knows is there is not a tooltip. The help cursor is the
   only signal a native title gets, and it costs nothing. */
.qf-cell[title], .ins-end[title] { cursor: help; }
.qf-k { font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; }
.qf-v { font-size:12px; font-weight:700; color:var(--text-main); font-variant-numeric:tabular-nums; }
.qf-age { font-size:10px; font-weight:600; }
.qf-fresh { color:var(--rs-green-deep); } .qf-ok { color:var(--text-muted); } .qf-stale { color:var(--rs-gold-deep); }
.qf-sep { color:var(--border-light); flex:0 0 auto; }
/* wrap + row-gap so the figures have somewhere to go when fitQuickFactsRow
   decides they do not fit; without the wrap they would simply overflow. */
.qf-row { display:flex; flex-wrap:wrap; align-items:center; gap:2px 8px; min-width:0; position:relative; }
/* A line of its own, starting at the left edge — under Fill/VP, in the space
   that was empty anyway. margin-left:auto is what parks it at the far right
   on a shared line, and it is exactly what has to go when it gets its own. */
.qf-row.is-stacked .quick-facts { margin-left:0; }
/* Controls lead, figures trail. Set with `order` rather than by moving the
   markup: #btnRefreshPrices and #chartTools are looked up by id from several
   places and #quickFacts has its innerHTML rewritten every price tick, so
   source order is load-bearing and layout order is not.
   The controls sit flush at the row's left edge, under the chart's own left
   edge, and the figures are pushed to the right by the auto margin. */
.qf-row .chart-tools { order:2; }
.qf-row .qf-refresh  { order:3; }
/* Content-sized, NOT stretched. At flex:1 1 auto the strip claimed the whole
   row; with margin-left:auto it takes only what it needs and parks at the far
   end regardless of how wide the row is.
   min-width:0 stays: the strip still has to be allowed to shrink below its
   content width and scroll sideways on a phone. */
.qf-row .quick-facts { order:5; flex:0 1 auto; min-width:0; margin-left:auto; }
/* ── 30-Day Range, inline in this row ────────────────────────────────────
   The meter used to hold a band of its own directly under this one, which
   cost ~70px of page for a bar, two numbers and a title — while this row
   ran 800px of empty between Refresh and the figures at 1440. placeItem-
   Insight puts the real node in here on the layouts where that gap exists;
   the block form below still applies everywhere else (phone portrait, where
   it lives in the sidebar).
   One line, so the whole thing fits the row's existing 28px: the ends move
   from under the track to either side of it, and the two things that only
   made sense stacked are dropped — the "30-DAY LOW/HIGH" sublabels (the
   title says Range and the ends are self-evidently its ends) and the
   floating live-price bubble, which would overhang into the chart and is
   the same figure as the hero price two rows up. */
/* The one-line meter, keyed to a CLASS rather than to .qf-row, because it is
   now used in two positions: inside the quick-facts row on desktop, and as
   its own full-width line directly under that row on a landscape phone. Same
   presentation, two places — placeItemInsight owns which. */
.item-insight.is-inline {
  max-width:none; margin:0; padding:0; border-top:0; text-align:left;
}
.item-insight.is-inline .ins-meter {
  max-width:none; margin:0; display:flex; align-items:center; gap:10px;
}
/* order:-2 so the title still LEADS. The low-end value is pulled in front of
   the track with order:-1, and without this the title (default order 0)
   ended up behind it — "1,907 30-DAY RANGE ▸" rather than
   "30-DAY RANGE 1,907 ▸". */
.item-insight.is-inline .ins-title { order:-2; padding:0; white-space:nowrap; flex:0 0 auto; }
/* min-width, not a fixed one: the bar is the only thing here that should
   spend the leftover, but it must also be allowed to give it back before
   the line overflows. */
.item-insight.is-inline .ins-track { order:0; flex:1 1 auto; min-width:70px; margin-top:0; }
/* display:contents so the two ends become items of .ins-meter itself and
   can be ordered around the track — otherwise they stay trapped in their
   own box, which is the box that put them underneath it. */
.item-insight.is-inline .ins-ends { display:contents; }
.item-insight.is-inline .ins-end    { order:-1; flex:0 0 auto; }
.item-insight.is-inline .ins-end-hi { order:1; }
.item-insight.is-inline .ins-elabel,
.item-insight.is-inline .ins-price { display:none; }
.item-insight.is-inline .ins-val { font-size:11.5px; }
/* Only the in-row copy is a flex child of the row and needs to behave as one. */
.qf-row .item-insight { order:4; flex:1 1 auto; min-width:0; }
/* Landscape phone: its OWN line under the row rather than inside it. Sharing
   the row worked at desktop widths but not here — the main column is barely
   600px, and once Fill/VP/Refresh, the meter and Spread/Vol were all on it
   the figures were running under the sidebar. A line of its own costs ~19px
   and the row stops fighting. */
@media (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  .item-insight.is-inline { margin-top:4px; }
}
/* A hairline, because "right before the stats" and "part of the stats" are
   one gap apart. The strip separates its own figures with "·", so a button
   sitting 8px from the first one would just read as another reading. The rule
   says the controls end here and the data begins.
   On Refresh, which is the last control at every width — the puck is hidden
   above 1024px and the toggles are hidden below it, but Refresh is always
   there and always order:3. No width-keyed duplicate needed, and no
   :first-of-type either: both controls are <button>, and display:none does
   not take an element out of that selector's reckoning. */
.qf-row .qf-refresh { position:relative; margin-right:6px; }
.qf-row .qf-refresh::after {
  content:''; position:absolute; right:-8px; top:3px; bottom:3px;
  width:1px; background:var(--border-main);
}
/* …except below 1024px, where Share joins this row and becomes the last
   control instead. The rule marks the boundary, not the button, so it moves
   with whatever is actually last — left on Refresh it would have fenced off
   Share along with the figures it is not. */
.qf-row:has(#btnShare) .qf-refresh { margin-right:0; }
.qf-row:has(#btnShare) .qf-refresh::after { content:none; }
.qf-row #btnShare { position:relative; margin-right:6px; }
.qf-row #btnShare::after {
  content:''; position:absolute; right:-8px; top:3px; bottom:3px;
  width:1px; background:var(--border-main);
}
.qf-refresh { flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center; gap:5px;
  width:26px; height:26px; padding:0; border:1px solid var(--border-main); border-radius:6px;
  background:transparent; color:var(--text-muted); cursor:pointer; transition:color .15s ease, border-color .15s ease, transform .12s ease; }
.qf-refresh:hover { color:#fff; border-color:var(--border-active); }
.qf-refresh:active { transform:translateY(1px); }
/* Icon-only by default: on a phone the quick-facts strip is narrow and a
   word here would crowd the two figures it carries. The roomy layout has
   the space, and this button does two different jobs — the label is what
   tells you which one is armed without hovering for the tooltip. */
.qf-rlabel { display:none; }
/* Chart view toggles. Same 26px height as .qf-refresh beside them so the
   group sits on one line; the chrome is deliberately NOT the same — see
   .ct-btn below.
   Inline at EVERY width. They spent a long time hidden behind a puck below
   1024px, on the reasoning that the quick-facts row couldn't carry two more
   chips — which was true while that row was also printing the live
   insta-buy/sell pair and the buy limit. It prints Spread and Vol now, so
   the two toggles cost less than the puck saved: measured at 375px, the
   narrowest layout, the row needs 293 of 367px with both inline.
   No margin-left: the toggles are the row's first item, so any indent here
   would push them off the chart's left edge above them. */
.chart-tools { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
/* The .ct-puck block lived here: a 26px button that opened these same two
   toggles as a popover below 1024px. Deleted with the popover — the toggles
   are inline at every width now, so the puck was a lid on an open box. Its
   JS went with it (placeChartTools/wireChartPuck and their media query),
   which also retires the 641-1023px band that once had neither the inline
   toolbar nor a working puck. */
/* Bare words, no chip — the same reasoning as the Share glyph in the Analyst
   Rating header (.rg-top .fav-btn): a border around a 10px label makes a box
   that is mostly empty, and two of them sitting against Refresh's box turned
   the corner of the row into three competing outlines. The padding stays so
   the tap target does not shrink to the text; the box goes, the target does
   not, and colour carries the whole affordance.
   Muted at rest and white on hover, exactly as Share behaves. The one place
   this has to go further is the ON state: Share is an action and never has
   one, while these are toggles and would otherwise be unreadable — so ON is
   gold, which is already this page's "you chose this" everywhere else (the
   active timeframe pill, the saved star). No fill and no glow behind it;
   the word alone changes colour. */
.ct-btn {
  height: 26px; padding: 0 6px; border: 0; border-radius: 4px;
  background: transparent; color: var(--text-muted); cursor: pointer;
  font-family: inherit; font-size: 10.5px; font-weight: 700; letter-spacing: .2px;
  transition: color .15s ease;
}
.ct-btn:hover, .ct-btn:focus-visible { color: #fff; }
.ct-btn[aria-pressed="true"] { color: var(--buy-color); }
.ct-btn[aria-pressed="true"]:hover { color: #FFD98A; }
/* The .qf-refresh.is-float rule lived here: on a phone the button was
   absolutely positioned inside the chart, and the comment that went with it
   was three sentences of arithmetic about which band of the canvas it could
   sit on without covering the x-axis labels or the price pane. That is the
   shape of a control with nowhere to go, and it had nowhere to go because
   the quick-facts row was full of numbers printed elsewhere. It isn't now,
   so the button is back in the row at every width and none of that
   arithmetic is needed. */
@media (min-width: 1024px) {
  .qf-refresh { width:auto; padding:0 10px; }
  .qf-rlabel { display:inline; font-size:10.5px; font-weight:700; letter-spacing:.2px; white-space:nowrap; }
}
/* Armed state: something is off-default (chart zoomed or panned, or a target
   price hand-nudged), so the button now also resets. Gold because that's the
   app's "this is actionable" colour everywhere else; the slow pulse is what
   catches the eye on a phone, where the button sits in a dense strip of
   numbers. */
.qf-refresh.is-dirty { color:var(--border-active); border-color:var(--border-active); animation:qf-armed 2.4s ease-in-out infinite; }
.qf-refresh.is-dirty:hover { background:var(--border-active); color:var(--accent-ink); animation:none; }
@keyframes qf-armed {
  0%, 100% { box-shadow:0 0 0 0 rgba(201,166,77,0); }
  50%      { box-shadow:0 0 0 3px rgba(201,166,77,0.16); }
}
@media (prefers-reduced-motion: reduce) {
  .qf-refresh.is-dirty { animation:none; box-shadow:0 0 0 2px rgba(201,166,77,0.18); }
}
.qf-refresh .ui-ic { width:13px; height:13px; display:block; }
/* Per-item auto-summary: a 30-day POSITION meter + one crawlable sentence of
   live market context. Neutral single-hue track (warm obsidian → champagne)
   deliberately NOT the gold/teal buy-sell language — it shows "where in the
   range", not a recommendation, so it can't be confused with the Analyst
   Rating gauge below it. The champagne marker is the only accent. */
/* 30-day range bar. Portrait stacks
   every module into one column, and this paragraph is the longest thing
   between the watchlist and the flip finder — useful the first few times you
   look at an item, dead weight once you can read the bar itself. The bar
   stays either way; only the prose folds away.
   Scoped to portrait: the control is hidden and the collapsed class inert at
   every other size, where the block sits under the chart with room to spare.
   Stored per layout via collapseKey, so folding it on a phone never reaches
   desktop. */
.item-insight { margin:8px 2px 0; padding-top:8px; border-top:1px solid var(--border-main); text-align:center; position:relative; }

@media (max-width: 640px) {
}
.ins-meter { max-width:440px; margin:0 auto; }
/* The one block on the page that had no name. Every other section says what
   it is — Potential Profit, Analyst Rating, Recommended Flip, Favorites,
   Find Opportunities — and this one had only an aria-label, so it read to
   sighted users as two loose numbers under the chart. It got away with that
   while a written summary sat beneath it; once that was deleted there was
   nothing left naming the thing.
   Same recipe as those five headers, left-aligned so it sits directly over
   the "30-Day Low" end label rather than floating centred above a bar whose
   ends are the actual content. */
.ins-title {
  /* PADDING, not margin. The live-price bubble floats above the track on a
     26px reserve and overhangs the top of it by 5px, so the title needs
     clearance — but a margin-bottom here just collapses into that 26px
     margin-top and buys nothing (max(12, 26) is still 26). Padding does not
     collapse, so this is the version that actually moves the track down. */
  text-align:left; margin-bottom:0; padding-bottom:10px;
  font-size:var(--hdr-size); font-weight:var(--hdr-weight); letter-spacing:var(--hdr-track);
  text-transform:uppercase; color:var(--text-structural);
}
/* The track reserves clear space ABOVE for the floating price bubble (there's
   no caption in that lane anymore, so the bubble can never cover it). */
.ins-track { position:relative; height:10px; border-radius:6px; margin-top:26px;
  background:linear-gradient(90deg, #211d17 0%, #443d31 55%, #6f6350 100%);
  border:1px solid var(--border-main);
  box-shadow:inset 0 1px 2px rgba(0,0,0,0.55), inset 0 -1px 0 rgba(255,255,255,0.04), 0 0 0 1px rgba(201,166,77,0.05); }
/* Subtle neutral "position in range" fill from the floor up to the marker —
   warm champagne, deliberately NOT a buy/sell tint. */
.ins-fill { position:absolute; left:0; top:0; bottom:0; width:0;
  border-radius:6px 0 0 6px;
  background:linear-gradient(90deg, rgba(201,166,77,0.10), rgba(201,166,77,0.28));
  transition:width .3s ease; pointer-events:none; }
.ins-marker { position:absolute; top:50%; left:50%; width:5px; height:20px; border-radius:3px; z-index:2;
  background:linear-gradient(180deg, #ffe6a8, var(--accent-bright));
  box-shadow:0 0 0 2px var(--bg-chart), 0 0 10px rgba(229,193,88,0.55), 0 1px 3px rgba(0,0,0,0.7);
  transform:translate(-50%,-50%); transition:left .3s ease; }
/* live price bubble that rides above the marker, with a small pointer */
.ins-price { position:absolute; bottom:calc(100% + 8px); left:50%; transform:translateX(-50%);
  background:var(--bg-panel); border:1px solid var(--border-light); border-radius:5px;
  padding:2px 7px; font-size:11px; font-weight:800; color:var(--accent-bright);
  font-variant-numeric:tabular-nums; white-space:nowrap; letter-spacing:.2px;
  box-shadow:0 2px 6px rgba(0,0,0,0.5); }
.ins-price::after { content:""; position:absolute; top:100%; left:50%; transform:translateX(-50%);
  border:4px solid transparent; border-top-color:var(--border-light); }
.ins-ends { display:flex; justify-content:space-between; align-items:flex-start; margin-top:8px;
  font-variant-numeric:tabular-nums; }
.ins-end { display:flex; flex-direction:column; align-items:flex-start; line-height:1.15; }
.ins-end-hi { align-items:flex-end; text-align:right; }
.ins-val { color:var(--text-main); font-weight:800; font-size:12.5px; }
.ins-elabel { color:var(--text-muted); font-size:8px; font-weight:600; text-transform:uppercase; letter-spacing:.5px; margin-top:1px; }
/* Desktop: a full-width "market context" band spanning the chart width. The
   meter and the summary sit SIDE BY SIDE so the width is actually used,
   instead of a narrow left-aligned box floating under a wide chart. Placed
   after the base .ins-* rules so these overrides win on source order. */
@media (min-width: 1024px) {
  /* Was a two-column band (meter | divider | summary). With the summary gone
     the bar keeps the row to itself, capped so it doesn't stretch to a
     hairline across a wide monitor. */
  .item-insight { margin: 0; padding: 16px 2px 0; border-top: 1px solid var(--border-main); }
  /* Title BESIDE the bar here, not above it. Stacked, it costs the block a
     whole line of height — and this row sits directly under the chart, so
     every pixel it takes is a pixel the chart gives up. There is nothing to
     the bar's left but empty column, so the title goes there and the block
     gets its line back. Left, not right: the right end carries the live
     price bubble and the 30-Day High figure, which is the busy end.
     Grid rather than flex so the two right-hand rows (track, then the
     lo/hi ends) stay stacked against a single full-height title cell,
     without wrapping them in an extra div.
     620 -> 740 so the BAR keeps its length; the title spends the width the
     block was not using rather than taking it from the scale. */
  .ins-meter {
    max-width: 740px; margin: 0 auto;
    display: grid; grid-template-columns: auto 1fr; column-gap: 18px;
  }
  .ins-title {
    grid-column: 1; grid-row: 1 / span 2; align-self: center;
    padding-bottom: 0; white-space: nowrap;
  }
  /* The bubble-clearance padding above is for the stacked layout only —
     side by side, the title and the bubble are in different columns. */
  .ins-track { grid-column: 2; grid-row: 1; }
  .ins-ends  { grid-column: 2; grid-row: 2; }
}
/* A landscape-mobile block lived here, sizing the card for a band just ABOVE
   the about-section — i.e. below the whole app — because the app is pinned to
   100dvh and the stacked card had nowhere to go inside it. The inline form
   fits the quick-facts row at 844px (measured: 319px of meter in 268px of
   spare row, track 142px), so the card is back inside the app and this block
   has nothing left to size. */
/* Timeframe row: stays on the SAME line as the name/Save at every width —
   see the nowrap note on .ticker-header. This block used to be what pushed
   it onto a second line (flex-basis:100% + margin-top), sold as "dressing up
   a wrap that was already unavoidable". It wasn't: basis 100% forces the
   wrap unconditionally, so the header took a second row at 1023px wide even
   though the same six buttons fit in 266px one breakpoint later.
   All that's left here is a slightly tighter gap once the header is narrow.
   The real narrow-width work is the sizing block keyed to the same container
   further up, plus the toolbar's own overflow-x. */
@container tickerhdr (max-width: 700px) {
  .ticker-header .toolbar { gap: 3px; }
}
/* Tightest band — a narrow window with the sidebar out leaves the header only
   ~340px. Buy the pill strip room the same way the phone layouts already do:
   drop the Save button's word (the filled gold star says it on its own) and
   shrink the item sprite. Worth ~60px, which is the difference between two
   pills visible and four.
   620px, not the 480 this started at. 480 was derived from a 650gp item, where
   the title group needs ~120px. It is the wrong number for the same header
   showing a 59,930,778 price, where the group's content floor is ~250px and
   there is no spare width at all — that item overlapped its own Save button.
   Since the floor is content-driven now, the threshold has to assume the
   expensive case, not the cheap one. The star alone is a familiar affordance
   and both phone layouts already ship it wordless. */
@container tickerhdr (max-width: 620px) {
  .ticker-header .fav-btn .fav-label,
  .ticker-header .fav-btn .fav-label-active { display: none; }
  /* 5px -> 3px vertical. This rule sits AFTER the per-layout ones and so wins:
     with 5px the star measured 28-29px against 26px pills (5+5+16 icon+2
     border), which is the whole mismatch. Horizontal padding stays wider than
     vertical so the tap target keeps its width. */
  .ticker-header .fav-btn { padding: 3px 7px; gap: 0; }
  .ticker-title-group { gap: 7px; }
  #tickerHeaderIconLink { width: 30px; height: 30px; }
  #tickerHeaderIconLink img { width: 24px; height: 24px; max-width: 24px; max-height: 24px; }
  /* The 24h change chip is the same number the 1D pill already shows, two
     inches to the right. Dropping the duplicate is ~50px back to the pills at
     exactly the width where they're starved. */
  .ticker-header .ticker-price-row .change { display: none; }
  /* "gp" was wrapping under a 10-digit price and pushing the row to two lines. */
  .ticker-header .ticker-price-row { flex-wrap: nowrap; white-space: nowrap; }
  /* The hero price sets the group's floor now, so its font size is directly
     how much room the timeframe pills get. At 22px a 10-digit price claimed
     227px of a 338px header and left the pills 47px — one pill. 17px is still
     the largest thing on the row and hands ~60px back. The exact figure is
     never lost: the Target Sell box's caption carries the live insta-buy. */
  .ticker-header .ticker-price-row .price { font-size: 17px; }
}
/* The two compact layouts (phone portrait, phone landscape) want the OPPOSITE
   of the desktop overflow rule: there the item name is hidden entirely, so the
   six buttons own the row and should stretch to fill it — tap targets, not a
   scroll strip. Their own blocks say `.toolbar button { flex: 1 }`, but the
   desktop rule is `.ticker-header .toolbar button`, one class more specific,
   so it would win everywhere and shrink the phone pills back to content width.
   Re-asserted here at matching specificity, after it in source order. */
@media screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  .ticker-header .toolbar { flex: 1 1 auto; gap: 5px; overflow: visible; }
  .ticker-header .toolbar button { flex: 1 1 0; min-width: 0; }
}
@media screen and (max-width: 640px) {
  .ticker-header .toolbar { flex: 1 1 auto; overflow: visible; }
  .ticker-header .toolbar button { flex: 1 1 0; min-width: 0; }
  /* Star sized to ITS OWN layout's pills. Portrait pills run ~21px, landscape
     pins them to 30 — so a single figure in the shared container query made the
     star oversized in one view and undersized in the other. It was 30 against
     21px pills here, the 9px mismatch reported.
     Exact height, not a min-height, and padding zeroed on the vertical: the
     container query above sets 3px, which with the 16px star and the 1px
     border still measured 24 against 21px pills. The 3px difference was small
     enough to argue for on tap-target grounds and it was still visibly wrong,
     which is the answer — the row's height is set by the toolbar either way
     (see the margin-top note below), so matching the pills costs nothing. */
  .ticker-header .fav-btn { height: 21px; min-height: 0; padding: 0 7px; box-sizing: border-box; }
}
@media screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  .ticker-header .fav-btn { min-height: 30px; box-sizing: border-box; }
}
/* Below ~500px of header, the pills get their own full-width row.
   This is NOT the unconditional wrap that was removed earlier — that one fired
   at a 1023px viewport where the six buttons needed 266px and had far more,
   costing ~50px of chart for nothing. Here they genuinely do not fit: with an
   expensive item the title group's content floor alone is ~200px, and once the
   strip is down to ~110px of visible width the scroll treatment stops being a
   graceful fallback. Measured on a 13.5M item at a 380px header, 1D was
   scrolled off the left AND 5Y off the right — the two ends of the range, the
   most likely things to want, both invisible at once. A row that shows all six
   beats a row that shows three and hides the rest behind a gesture nobody is
   told about.
   500px is where it flips: title floor ~200 + star-only Save 32 + the compact
   toolbar's natural 246 + gaps ~24. Above that they share a line as before. */
/* Desktop-narrow ONLY. Phone portrait (<=640px) has its own header layout that
   deliberately pins the star and the six pills to one row with the search box,
   and hides the item name entirely to pay for it. Letting this wrap rule reach
   that layout put the star alone on its own line and took the header from 30px
   to 71px — 41px straight off the chart on the smallest screens. */
@media screen and (min-width: 641px) {
  @container tickerrow (max-width: 500px) {
    .ticker-header { flex-wrap: wrap; }
  }
}
@container tickerhdr (max-width: 500px) {
  .ticker-header .toolbar {
    flex: 1 1 100%; margin-top: 6px; gap: 4px;
    overflow: visible;            /* full width — nothing left to scroll */
  }
  .ticker-header .toolbar button { flex: 1 1 0; min-width: 0; }
}
/* …but the margin above is for the DESKTOP-narrow case, where the rule above
   genuinely puts the pills on their own second row and they need separating
   from the name. Both phone layouts keep the header `nowrap`, so there the
   pills sit BESIDE the star on one row and that 6px is not a gap between two
   rows — it is 6px of dead space added to the header's height, and it drops
   the pills 6px below the star they are supposed to line up with.
   Measured: portrait header 33px tall for 27px of content, landscape 46 for
   40. Zeroing it is the single largest piece of slack in this stack.
   Placed after the container query because neither has any specificity of its
   own — source order is the whole mechanism. */
@media screen and (max-width: 640px),
       screen and (max-height: 600px) and (orientation: landscape) and (pointer: coarse) {
  .ticker-header .toolbar { margin-top: 0; }
}
/* ── Target box: caption above, number below ─────────────────────────────
   Applied by fitPriceNums when the number would starve its own caption, not
   by width alone — the trigger is the item's price, and CSS cannot see that.
   Measured on a 13.5M item: the caption was truncated at EVERY width below
   1280, and at a 209px box it was 27px wide with the title wrapped onto three
   lines. Earlier audits missed it because they only checked whether the NUMBER
   fit; it always did. The caption was the thing paying.
   Placed last so it wins over each band's own .price-box rules. */
/* Guarded to >=641px, i.e. everywhere the -/+ steppers exist. Phone portrait
   hides them, so there the "value row" is a bare number and giving it its own
   line costs ~21px per box and buys nothing. That layout already solves this
   problem its own way — 8px caption that ellipsises, value that never shrinks —
   and it was tuned for that. Without the guard the boxes stacked at 375 and 390
   but not at 430, because the 148px threshold below is calibrated against the
   desktop 10px caption and the phone's is 8px. */
@media screen and (min-width: 641px) {
  .price-box.is-stacked { display: block; }
  /* Caption on ONE row, number under it — two lines, not three.
     The first version stacked the title above the live quote, which made a
     three-line box for what is really one caption and one number. They sit
     side by side now: the title is short and fixed, the quote takes the rest
     and ellipsises, which is the same priority the inline layout uses. */
  .price-box.is-stacked .price-label {
    display: flex; flex-direction: row; align-items: baseline;
    gap: 8px; width: 100%; min-width: 0; flex-shrink: 1;
  }
  /* .pl-t exists so this rule has something to select. The title used to be a
     bare TEXT NODE beside the .live-val span, and a text node in a flex
     container becomes an ANONYMOUS flex item — unstylable and unselectable, so
     ":first-child" here would have matched .live-val and pinned exactly the
     wrong half. */
  .price-box.is-stacked .price-label .pl-t { flex: 0 0 auto; }
  .price-box.is-stacked .price-label .live-val {
    flex: 0 1 auto; min-width: 0; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .price-box.is-stacked .price-val {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; margin-left: 0; margin-top: 3px;
  }
}
