SPB Git

spb/rareindex

Public
27.3 KB · 374 lines tsx
1import type { Metadata } from 'next';2import Link from 'next/link';3import type { ReactNode } from 'react';4import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, DEAL_REVIEW_THRESHOLD, DEAL_THRESHOLD, MAX_PLAUSIBLE_CHANGE, PREMIUM_THRESHOLD } from '@rareindex/valuation';5import { PageHeader } from '@/components/ui/page-header';6import { Badge, Card } from '@/components/ui/primitives';7import { cn } from '@/lib/format';89export const metadata: Metadata = {10  title: 'Methodology Center',11  description: 'How every RareIndex number is produced: RareIndex Valuation (RIV), confidence, value range, comparables, the ask-vs-RIV anomaly gate, liquidity, rarity, momentum, outliers, sales verification, indices, FX and data-honesty rules.',12};13export const revalidate = 3600;1415/*16 * §203 Methodology Center. Constants that gate numbers on the site are imported from @rareindex/valuation so17 * this page cannot drift from the implementation. Constants that live as literals in the engine files are18 * quoted with their file so a reader can verify them (valuation.ts, scores.ts, outliers.ts, premiums.ts,19 * indices/chain.ts, workers/indices/run.ts).20 */2122const pct = (v: number, digits = 0) => `${(v * 100).toFixed(digits)} %`;2324const SECTIONS: Array<{ id: string; title: string }> = [25  { id: 'principles', title: 'Data honesty' },26  { id: 'riv', title: 'RIV — RareIndex Valuation' },27  { id: 'confidence', title: 'Confidence' },28  { id: 'range', title: 'Value range' },29  { id: 'distribution', title: 'Price distribution' },30  { id: 'comparables', title: 'Comparables & grade premiums' },31  { id: 'ask-vs-riv', title: 'Ask vs RIV & the anomaly gate' },32  { id: 'liquidity', title: 'Liquidity Score' },33  { id: 'rarity', title: 'Rarity Score' },34  { id: 'momentum', title: 'Momentum & Trending' },35  { id: 'outliers', title: 'Outliers' },36  { id: 'verification', title: 'Sales verification' },37  { id: 'indices', title: 'Indices' },38  { id: 'fx', title: 'Currencies' },39  { id: 'freshness', title: 'Freshness & limits' },40];4142export default function MethodologyPage() {43  return (44    <div>45      <PageHeader46        kicker="Transparency"47        title="Methodology Center"48        description="Every number on RareIndex is either observed data, a model estimate, user data or an external asking price — and is labelled as such. This page documents how each estimate is produced, what evidence it rests on and the exact thresholds that gate it. Thresholds are read from the valuation engine, not copied by hand."49        meta={50          <>51            <Link href="/data" className="hover:text-fg">52              Coverage & sources →53            </Link>54            <Link href="/rareindex" className="hover:text-fg">55              Indices →56            </Link>57            <Link href="/api-docs" className="hover:text-fg">58              API →59            </Link>60          </>61        }62      />63      <div className="grid gap-6 lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start">64        <nav aria-label="Methodology sections" className="hidden lg:block lg:sticky lg:top-[calc(var(--ri-header-h)+16px)]">65          <ol className="border-l border-border text-[12px]">66            {SECTIONS.map((s, i) => (67              <li key={s.id}>68                <a href={`#${s.id}`} className="-ml-px flex items-baseline gap-2 border-l border-transparent py-1 pl-3 text-muted hover:border-fg hover:text-fg">69                  <span className="num w-4 shrink-0 text-[10px] text-subtle">{i + 1}</span>70                  {s.title}71                </a>72              </li>73            ))}74          </ol>75        </nav>76        <div className="grid gap-4">77          <div className="lg:hidden">78            <details className="card p-3 text-[12px]">79              <summary className="cursor-pointer font-medium text-fg">Sections</summary>80              <ol className="mt-2 grid gap-1 pl-4 text-muted">81                {SECTIONS.map((s) => (82                  <li key={s.id}>83                    <a href={`#${s.id}`} className="hover:text-fg">84                      {s.title}85                    </a>86                  </li>87                ))}88              </ol>89            </details>90          </div>9192          <Section id="principles" n={1} title="Data honesty" lead="Four kinds of numbers appear on the platform. They are never mixed, and a missing number is shown as such (§205–§206).">93            <div className="grid gap-2 sm:grid-cols-2">94              <Kind tone="gain" name="Observed data" desc="A sale, a listing, an auction lot, a population count or a guide price captured from a public source, stored immutably with its URL, capture time, connector and parser version. Shown with its source link." />95              <Kind tone="index" name="Model estimate" desc="RIV, its range and confidence, scores (liquidity, rarity, momentum, trending, deal), indices and returns. Always displayed with sample size, confidence and update time." />96              <Kind tone="rarity" name="User data" desc="Purchase prices, quantities, manual valuations and notes in collections and watchlists. Private by default; never used to compute market estimates." />97              <Kind tone="alert" name="External ask" desc="An asking price on a marketplace. Never a market value: it is compared with RIV only through the gate in §7 and labelled as an ask everywhere." />98            </div>99            <P>100              RareIndex never fabricates transactions, valuations, bids, populations, sales, historical prices or certification data. Where evidence is missing the interface shows <em>Data unavailable</em> or <em>Not enough data</em>. Valuations are estimates from observed public sales, not offers or appraisals; RareIndex does not authenticate items.101            </P>102          </Section>103104          <Section id="riv" n={2} title="RIV — RareIndex Valuation" lead="RIV estimates the current fair market value of a variant (an asset in one grade/condition) from verified transactions. It is never an arithmetic mean alone.">105            <H>Evidence and window</H>106            <P>107              Inputs are valid transactions of the variant: status <Code>valid</Code>, price &gt; 0, single item (bundles and multi-quantity lots excluded). The default window is <strong>365 days</strong>; when fewer than five recent sales exist and older ones do, it is extended to <strong>3 years</strong> and confidence is capped at 70 %.108            </P>109            <H>Estimators</H>110            <Table111              head={['Estimator', 'Definition']}112              rows={[113                ['Latest', 'most recent transaction in the window'],114                ['Median 5 / 10 / 20', 'median of the last 5, 10 and 20 transactions'],115                ['Trimmed mean', '10 % trimmed each tail; requires ≥ 5 sales'],116                ['EW mean', 'exponentially weighted mean, 90-day half-life'],117                ['Weighted mean', 'weight = source trust × identification confidence × 0.5^(age / 90 d)'],118                ['Comps-adjusted', 'median of other-grade sales × empirical grade premium (≥ 3 comps) — see §6'],119                ['Guide', 'median of the 5 most recent price-guide observations ≤ 60 days old (market / mid / trend kinds preferred)'],120              ]}121            />122            <H>Basis ladder</H>123            <Table124              head={['Basis', 'When', 'RIV', 'Confidence']}125              rows={[126                ['transactions', '≥ 3 sales in window', 'median of {EW mean, median 10 (or 5), weighted mean, trimmed mean (or median 5)}', 'computed (§3)'],127                ['transactions', '1–2 sales + comps or guide', 'median of {latest, comps or guide}', '0.45 (comps) · 0.40 (guide)'],128                ['transactions', '1–2 sales only', 'median of the two, or the single sale', '0.30 · 0.20'],129                ['comps', 'no sales, ≥ 3 grade-adjusted comps', 'comps-adjusted median', '0.25 + 0.03 × comps, ≤ 0.50'],130                ['guide', 'no sales, guide observations only', 'guide median', '0.20 + 0.05 × obs, ≤ 0.45'],131                ['none', 'nothing usable', 'Data unavailable', '—'],132              ]}133            />134            <P>135              A <strong>guide</strong> value is a price published by a price guide or marketplace (e.g. a “market price” cell). It is an observation, not a transaction; in the interface it appears as <em>Guide · source</em>, never as RIV, and guide-based RIVs are labelled with a capped confidence. The headline RIV of an asset is the valuation of its representative variant: the default (raw / base) variant when it has a transaction-based RIV with ≥ {ASK_MIN_SAMPLE} sales and confidence ≥ {pct(ASK_MIN_CONFIDENCE)}, otherwise the qualifying variant with the most sales. Period changes are measured on that variant&apos;s own series so a change of representative never shows as a price move.136            </P>137            <Ref>packages/valuation/src/valuation.ts · workers/valuation/run.ts</Ref>138          </Section>139140          <Section id="confidence" n={3} title="Confidence" lead="Every RIV carries a 0–1 confidence and a label. Users must be able to see how uncertain a valuation is.">141            <Formula>confidence = 0.35 · size + 0.30 · (1 − dispersion) + 0.20 · recency + 0.15 · trust</Formula>142            <Table143              head={['Term', 'Definition', 'Scale']}144              rows={[145                ['size', 'log2(n + 1) / log2(41)', '40 transactions → 1'],146                ['dispersion', 'robust σ of log prices = 1.4826 × MAD, divided by 0.6', '60 % log-dispersion → 0'],147                ['recency', '1 − age of the latest sale / 365 d', 'a year-old latest sale → 0'],148                ['trust', 'mean of source trust × identification confidence over the sales used', '0–1'],149              ]}150            />151            <P>152              Labels: <Badge tone="gain">High ≥ 75 %</Badge> <Badge tone="index">Medium ≥ 50 %</Badge> <Badge tone="alert">Low &gt; 20 %</Badge> <Badge>Insufficient</Badge>. Window extended to three years → capped at 70 %; comps-only ≤ 50 %; guide-only ≤ 45 %.153            </P>154            <Ref>packages/valuation/src/valuation.ts (confidence, confidenceLabel)</Ref>155          </Section>156157          <Section id="range" n={4} title="Value range — low / fair / high" lead="A point estimate is never presented as exact. Each RIV comes with an expected market range.">158            <Table159              head={['Sample', 'Low', 'High']}160              rows={[161                ['≥ 5 sales', 'min(P25 of the sales used, RIV × 0.95)', 'max(P75, RIV × 1.05)'],162                ['2–4 sales', 'RIV × e^(−max(σ, 0.1)) with σ = sd of log prices', 'RIV × e^(+max(σ, 0.1))'],163                ['1 sale / fallback', 'RIV × (1 − 20 %) — 30 % for guide-based', 'RIV × (1 + 20 %) — 30 % for guide-based'],164              ]}165            />166            <P>The band is always at least ±5 % around RIV. On asset pages it is drawn as the shaded band around the RIV line; the current RIV and latest sale are marked on the range meter.</P>167          </Section>168169          <Section id="distribution" n={5} title="Price distribution" lead="Beyond the point estimate, the dispersion of the transactions themselves is shown.">170            <P>For the variant and window, RareIndex reports count, minimum, P25, median, P75, maximum and the 10 % trimmed mean of verified sale prices in USD at historical FX. Flagged outliers (§11) are shown in sales tables for audit but excluded from these statistics.</P>171          </Section>172173          <Section id="comparables" n={6} title="Comparables & grade premiums" lead="When a variant has too few sales, sales of other grades of the same asset are used — adjusted, never mixed as-is.">174            <P>175              For each category, grader and grade, the <strong>grade premium</strong> is the median, across assets, of median(graded price) / median(raw price) of the <em>same asset</em>, from at least <strong>8 paired assets</strong>. A comparable from grade A to grade B is adjusted by premium(B) / premium(A). Premiums exist only where paired evidence exists: PSA 10, BGS 10 and CGC 10 are never assumed equivalent, and no cross-grader factor is applied without data.176            </P>177            <P>Comparable sales on asset pages carry a transparent similarity score (same set, same grade, same variant, price proximity). Grade curves (price by grade) and premiums are published on the Grades tab and on /grading.</P>178            <Ref>packages/valuation/src/premiums.ts (computeGradePremiums, adjustmentFactor)</Ref>179          </Section>180181          <Section id="ask-vs-riv" n={7} title="Ask vs RIV & the anomaly gate" lead="An asking price is compared with a valuation only when the comparison is legitimate. An implausible ask is a data or identity problem, never a deal.">182            <Formula>discount = (ask + fees − RIV) / RIV &nbsp;·&nbsp; negative = below the valuation</Formula>183            <H>Gates — all must pass</H>184            <Table185              head={['Gate', 'Threshold']}186              rows={[187                ['Same variant', 'the ask is compared with the RIV of its own grade/condition variant; a slab whose grade could not be read is kept in a “grade unknown” variant and never compared against raw'],188                ['Valuation basis', 'transactions only — comps-only and guide-only RIVs never qualify an ask'],189                ['RIV confidence', `≥ ${pct(ASK_MIN_CONFIDENCE)}`],190                ['Transactions used', `≥ ${ASK_MIN_SAMPLE}`],191                ['Listing identification confidence', `≥ ${pct(ASK_MIN_MATCH_CONFIDENCE)}`],192                ['Plausibility band', `ask between ${ASK_ANOMALY_LOW_RATIO}× and ${ASK_ANOMALY_HIGH_RATIO}× RIV — outside it the listing is flagged riv_anomaly and shown as “Data/identity anomaly”`],193              ]}194            />195            <H>Verdicts</H>196            <Table197              head={['Verdict', 'Rule']}198              rows={[199                ['Deal', `discount ≤ ${pct(DEAL_THRESHOLD)} (ask at least ${pct(-DEAL_THRESHOLD)} below RIV)`],200                ['Fair', `between ${pct(DEAL_THRESHOLD)} and ${pct(PREMIUM_THRESHOLD)}`],201                ['Premium', `discount ≥ ${pct(PREMIUM_THRESHOLD)}`],202                ['Needs review', `more than ${Math.round(-DEAL_REVIEW_THRESHOLD * 100)} % below RIV — kept as a number, flagged riv_review, excluded from every deal rail, Deal Radar, digest and Rare Radar until reviewed (§174)`],203                ['Data/identity anomaly', `ratio outside ${ASK_ANOMALY_LOW_RATIO}×–${ASK_ANOMALY_HIGH_RATIO}× — wrong variant, lot, currency, typo or mismatch until reviewed`],204                ['Not compared', 'one of the gates failed; the reason is shown (e.g. riv_sample, riv_basis_guide)'],205              ]}206            />207            <H>Deal Score (0–100)</H>208            <Formula>score = 100 · depth^0.7 · (0.4 + 0.6 · confidence) · (0.5 + 0.5 · sample) · (0.5 + 0.5 · liquidity / 100) · match</Formula>209            <P>210              depth = min(1, −discount / 50 %), sample = log10(1 + n) / log10(41). The components are multiplicative: a huge discount on an illiquid or poorly identified asset cannot score high. The score is 0 for anything that is not a gated discount. Period changes of a valuation beyond ±{MAX_PLAUSIBLE_CHANGE * 100} % are likewise treated as data artefacts and not published as movers.211            </P>212            <P className="text-subtle">Value opportunities and Deal Radar are analytical data, not investment advice: a discount can also mean a misidentified, damaged or incomplete item.</P>213            <Ref>packages/valuation/src/scores.ts (assessAsk, dealScore, isPlausibleChange) · workers/valuation/run.ts</Ref>214          </Section>215216          <Section id="liquidity" n={8} title="Liquidity Score (0–100)" lead="How readily a variant trades. Null when there is no evidence at all — never 0 by default.">217            <Formula>liquidity = 100 · (0.40 · s₁ + 0.15 · s₂ + 0.10 · s₃ + 0.20 · s₄ + 0.15 · s₅)</Formula>218            <Table219              head={['Component', 'Input', 'Scale (clamped 0–1)', 'Weight']}220              rows={[221                ['s₁ sales pace', 'sales per month (12-month average at asset level; 36-month at variant level)', 'log10(1 + x) / log10(31) · 30 / month → 1', '40 %'],222                ['s₂ listing depth', 'active listings', 'log10(1 + x) / log10(51) · 50 → 1', '15 %'],223                ['s₃ source breadth', 'distinct sources with evidence', '(sources − 1) / 4', '10 %'],224                ['s₄ sale spacing', 'median days between sales', '1 − days / 90 · 0 when unknown', '20 %'],225                ['s₅ ask–RIV spread', '(lowest ask of the representative variant − RIV) / RIV', '1 − |spread| / 50 % · 0.5 when there is no ask', '15 %'],226              ]}227            />228            <P>Labels used in the interface: Highly liquid ≥ 75, Moderately liquid ≥ 50, Illiquid ≥ 25, Extremely illiquid below.</P>229            <Ref>packages/valuation/src/scores.ts (liquidityScore)</Ref>230          </Section>231232          <Section id="rarity" n={9} title="Rarity Score (0–100)" lead="A multi-dimensional supply score. It distinguishes documented supply (population, production) from market supply (how often the object appears).">233            <Table234              head={['Signal', 'Scale (clamped 0–1)', 'Weight']}235              rows={[236                ['Graded population (latest published report)', '1 − log10(1 + pop) / 5 · 100 000 → 0', '40 %'],237                ['Documented production quantity', '1 − log10(qty) / 7', '30 %'],238                ['Sales per year', '1 − log10(1 + sales) / 3 · 1 000 → 0', '20 %'],239                ['Listings per year (active × 4)', '1 − log10(1 + listings) / 3', '10 %'],240              ]}241            />242            <P>Weights are renormalised over the signals that exist for the asset; a population growing more than 20 % between reports multiplies the score by 0.9. With no signal at all the score is null. Population figures come only from published grading-company reports and are shown with their report date.</P>243            <Ref>packages/valuation/src/scores.ts (rarityScore)</Ref>244          </Section>245246          <Section id="momentum" n={10} title="Momentum & Trending" lead="Momentum measures how a valuation and its activity are changing; Trending blends momenta into one discovery signal.">247            <Formula>momentum(h) = 100 · (0.7 · clamp(Δprice / 50 %, ±1) + 0.3 · (volume_h − volume_prev) / max(1, volume_h + volume_prev))</Formula>248            <P>Horizons 7 d / 30 d / 90 d / 1 y; Δprice is the change of the representative variant&apos;s RIV against its own history. Trending is the geometric mean of the available momenta mapped to 0–1 (price 30 d, transaction volume, listing depth); it requires at least two components. Movers rails require ≥ {ASK_MIN_SAMPLE} transactions, confidence ≥ {pct(ASK_MIN_CONFIDENCE)}, ≥ 3 sales in the last year and a plausible move (≤ ±{MAX_PLAUSIBLE_CHANGE * 100} %).</P>249            <Ref>packages/valuation/src/scores.ts (momentumScore, trendingScore)</Ref>250          </Section>251252          <Section id="outliers" n={11} title="Outliers" lead="Abnormal prices are flagged, never deleted, and every flag is auditable.">253            <P>254              Within a variant with at least five valid sales, the modified z-score of each log price, 0.6745 · (x − median) / MAD, is computed; sales beyond <strong>3.5</strong> are flagged <Code>abnormally_high_price</Code> / <Code>abnormally_low_price</Code> and set to status <Code>flagged</Code> (when MAD is 0, anything beyond 3× the median is flagged). Bundles and multi-quantity lots are <Code>excluded</Code> at ingestion. Flagged and excluded sales stay visible in sales tables for audit and are written to the audit log with their reason; they do not enter RIV, distributions or indices.255            </P>256            <Ref>packages/valuation/src/outliers.ts · workers/valuation/run.ts</Ref>257          </Section>258259          <Section id="verification" n={12} title="Sales verification" lead="What “verified” means today — and what it does not.">260            <P>261              A sale is a completed-transaction record captured from a public source (auction result page, completed marketplace listing, price-guide sold feed). Each sale stores its <strong>status</strong> (<Code>valid</Code>, <Code>flagged</Code>, <Code>excluded</Code>), an <strong>identification confidence</strong> (0–1: the weaker of the record&apos;s own confidence and the entity-resolution confidence; deterministic identifiers 0.99, canonical key 0.96, fuzzy match ≤ 0.90), the <strong>trust score</strong> of its source (0–1, weighting valuations), <strong>flags</strong> (<Code>bundle</Code>, <Code>zero_price</Code>, <Code>low_identification_confidence</Code>, outlier reasons) and whether the buyer premium is known to be included.262            </P>263            <P>264              Record-sale and “verified transactions” views use status <Code>valid</Code> with confidence ≥ 0.8. Duplicate captures are collapsed by source, external id or URL, date and price. RareIndex does not yet publish a separate “likely / unverified” verification ladder or cross-source duplicate merging of the same physical transaction; where a transaction is only reported second-hand it appears as a price observation, not a sale.265            </P>266            <Ref>workers/entity-resolution/writers.ts · docs/METHODOLOGY.md §1–2</Ref>267          </Section>268269          <Section id="indices" n={13} title="Indices" lead="RARE and its subindices are chain-linked indices of daily RIV changes. They publish only once enough constituents exist.">270            <Table271              head={['Rule', 'Value']}272              rows={[273                ['Constituent eligibility', 'valuation confidence ≥ 0.4 and ≥ 3 valid sales in the last 12 months; one variant per asset (the most traded)'],274                ['Daily return', 'weighted mean of constituents’ log(RIVₜ / RIV_last observed); equal weights by default, liquidity weights (12-month sales) where configured'],275                ['Robustness', 'top and bottom 5 % of daily constituent returns trimmed when ≥ 20 returns; a single constituent’s daily return is capped at ±ln 2.5'],276                ['Publication', 'a day is published only when ≥ 10 constituents have a value that day (level carried otherwise); RARE requires ≥ 25 constituents across its published subindices'],277                ['Base', '1 000 on 2024-01-01'],278                ['RARE', 'transaction-weighted mean (weight = 1 + transactions) of the daily log returns of published subindices'],279                ['Composition', 'membership can change daily without breaking the level; additions and removals are stored with their reason'],280                ['Market capitalisation', 'Σ (graded population × RIV) only for assets with a published population report, with a coverage-based confidence'],281              ]}282            />283            <P>Indices without a published value are listed as <em>in development</em> with their constituent count against the minimum, outside the primary ticker. Every daily value stores its constituent count and coverage. A repeat-sales estimator exists for research and is not a published index.</P>284            <Ref>packages/indices/src/chain.ts · workers/indices/run.ts</Ref>285          </Section>286287          <Section id="fx" n={14} title="Currencies" lead="Native prices are never overwritten; every conversion is reproducible.">288            <P>289              Each sale stores <Code>price</Code>, <Code>currency</Code>, <Code>price_usd</Code>, <Code>fx_rate</Code> and <Code>fx_date</Code>. Rates are ECB reference rates (via frankfurter), base USD, applied at the <strong>transaction date</strong> (previous business day for weekends and holidays); historical sales are never converted at today&apos;s rate. A missing rate blocks the record rather than approximating it. Display currencies (USD, CAD, EUR, GBP, JPY, …) are converted at view time from the stored USD value.290            </P>291            <Ref>workers/fx.ts · workers/lib/fx.ts</Ref>292          </Section>293294          <Section id="freshness" n={15} title="Freshness & limits" lead="Coverage is uneven by design; the interface shows what it knows and when it learned it.">295            <P>Connectors run on schedules tuned to each source; every statistic shows its last update and sample size. Categories with few public transactions show fewer valuations and no index rather than a fabricated one. Listing prices are not confirmed transactions; past performance does not guarantee future results; RareIndex does not authenticate items and its valuations are not appraisals.</P>296            <P>297              <Link href="/data" className="text-fg underline-offset-4 hover:underline">298                Live coverage, connectors and FX ranges →299              </Link>300            </P>301          </Section>302        </div>303      </div>304    </div>305  );306}307308function Section({ id, n, title, lead, children }: { id: string; n: number; title: string; lead: string; children: ReactNode }) {309  return (310    <Card as="article" id={id} className="scroll-mt-16 p-4 sm:p-5">311      <header className="mb-3 border-b border-border pb-3">312        <p className="t-label">313          <span className="num">{String(n).padStart(2, '0')}</span>314        </p>315        <h2 className="mt-0.5 text-base font-semibold tracking-tight text-fg">{title}</h2>316        <p className="mt-1 text-[12.5px] leading-relaxed text-muted">{lead}</p>317      </header>318      <div className="grid gap-3 text-[13px] leading-relaxed text-muted">{children}</div>319    </Card>320  );321}322323function H({ children }: { children: ReactNode }) {324  return <h3 className="mt-1 text-[11px] font-semibold uppercase tracking-wider text-subtle">{children}</h3>;325}326function P({ children, className }: { children: ReactNode; className?: string }) {327  return <p className={cn('max-w-[78ch]', className)}>{children}</p>;328}329function Code({ children }: { children: ReactNode }) {330  return <code className="rounded-sm bg-inset px-1 text-[12px] text-fg">{children}</code>;331}332function Formula({ children }: { children: ReactNode }) {333  return <p className="mono-num rounded-sm border border-border bg-sunken px-3 py-2 text-[12px] text-fg">{children}</p>;334}335function Ref({ children }: { children: ReactNode }) {336  return <p className="mono-num text-[10.5px] text-subtle">Source: {children}</p>;337}338function Kind({ tone, name, desc }: { tone: 'gain' | 'index' | 'rarity' | 'alert'; name: string; desc: string }) {339  return (340    <div className="rounded-sm border border-border p-3">341      <Badge tone={tone}>{name}</Badge>342      <p className="mt-1.5 text-[12px] leading-relaxed text-muted">{desc}</p>343    </div>344  );345}346function Table({ head, rows }: { head: string[]; rows: string[][] }) {347  return (348    <div className="overflow-x-auto">349      <table className="w-full min-w-[520px] border-collapse text-[12px]">350        <thead>351          <tr className="border-b border-border text-left text-[10px] uppercase tracking-wider text-subtle">352            {head.map((h) => (353              <th key={h} className="py-1.5 pr-3 font-medium">354                {h}355              </th>356            ))}357          </tr>358        </thead>359        <tbody className="divide-y divide-border">360          {rows.map((r, i) => (361            <tr key={i}>362              {r.map((c, j) => (363                <td key={j} className={cn('py-1.5 pr-3 align-top', j === 0 ? 'font-medium text-fg' : 'text-muted')}>364                  {c}365                </td>366              ))}367            </tr>368          ))}369        </tbody>370      </table>371    </div>372  );373}374
Files · b5d6968a47f5d2546937866bdabc967739349047
Menu

Theme