diff --git a/astro.config.mjs b/astro.config.mjs index 574dbc3..270f16f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -29,7 +29,11 @@ export default defineConfig({ !page.includes('/study/review') && !page.includes('/study/settings') && !page.includes('/study/challenges') && - !page.includes('/study/cases'), + !page.includes('/study/cases') && + // PLAYGROUND_ENABLED (src/lib/feature-flags.ts) is false — the route + // renders a "temporarily unavailable" page, so keep it out of the + // sitemap. Remove this line when the flag flips back to true. + !page.includes('/playground'), lastmod: new Date(), }), ], diff --git a/docs/dark-mode.md b/docs/dark-mode.md new file mode 100644 index 0000000..6fbda35 --- /dev/null +++ b/docs/dark-mode.md @@ -0,0 +1,239 @@ +# Dark Mode — Sweep Cheat-Sheet + +Status: **foundation complete** (tokens, activation, toggle, global base styles, +UI kit). Page-level sweeps are the next phase — this document is the reference +those sweeps should follow. It assumes you're comfortable with Tailwind's +`dark:` variant; the only non-standard thing here is *how* dark mode is +activated (attribute, not the default `.dark` class) and the site-specific +token names. + +Read this top to bottom before sweeping a page. The "hard gates" section at +the end lists things that must never happen. + +## 1. How activation works + +- `darkMode: ['selector', '[data-theme="dark"]']` in `tailwind.config.mjs` + (Tailwind 3.4.15 `selector` strategy). Every `dark:` utility compiles to + `[data-theme="dark"] &`. It is **not** the classic `.dark` class strategy. +- `data-theme="light"` or `data-theme="dark"` is set on `` by an inline, + `is:inline` no-FOUC script in `src/layouts/BaseLayout.astro`, which runs + before first paint. Priority: `localStorage.theme` → `prefers-color-scheme` + fallback. +- `window.__setTheme(theme, persist = true)` is the single source of truth for + changing theme at runtime. It sets the attribute, persists to + `localStorage` (unless `persist` is `false`), and fires a + `document.dispatchEvent(new CustomEvent('themechange', { detail: { theme } }))`. + **Never** set `data-theme` directly from page code — call `__setTheme`, or + read the current value via `document.documentElement.getAttribute('data-theme')`. +- Cross-tab sync: a `storage` event listener re-applies theme when + `localStorage.theme` changes in another tab. +- OS-preference sync: a `matchMedia('(prefers-color-scheme: dark)')` listener + only fires when there is **no stored preference** — once a user has an + explicit choice, OS changes are ignored (by design). +- `color-scheme` (native form controls/scrollbar) is set via CSS in + `global.css`, tied to `[data-theme='dark']`, not a static meta tag — so it + always matches the *active* theme, not raw OS state. + +## 2. Toggle architecture + +`src/components/layout/ThemeToggle.astro` is framework-free (no React) and +safe to render multiple times per page (desktop header + mobile menu both do +this). Multiple instances share: + +- one delegated `click` listener bound once globally (guarded by + `window.__themeToggleBound`) +- one `themechange` listener that re-syncs `aria-pressed` on every instance + via the shared `.theme-toggle` class + +If you need a new toggle placement, just drop `` in — don't +write new JS for it. + +## 3. Token reference + +Defined in `tailwind.config.mjs`. Hex values are used everywhere (not +`oklch()` strings) for opacity-modifier (`/10`, `/20`, …) compatibility across +browsers; each token's OKLCH source is documented as an inline comment next to +its hex value in the config. + +| Token | Hex | Use | +|---|---|---| +| `coffee-sunk` | `#0f0704` | inputs/wells, code blocks, recessed surfaces | +| `coffee-page` | `#190f09` | `` background | +| `coffee-raised` | `#271d15` | cards, panels, header/footer surfaces | +| `coffee-raised2` | `#332619` | popovers, dropdowns, hover-raised state | +| `coffee-border` | `#362b23` | hairline borders, dividers | +| `coffee-border-strong` | `#4d3f33` | emphasized borders, focus-adjacent | +| `cream-primary` | `#ede3d5` | headings, primary text | +| `cream-secondary` | `#c4b4a3` | body copy, descriptions | +| `cream-muted` | `#8f8578` | captions, meta text, hints | +| `cream-inverse` | `#f3eadd` | **footer only** — see cream-inverse-band rule below | +| `gold` | `#d8a16c` | link-hover underline decoration + blockquote/citation accents ONLY | +| `crystal-400`/`crystal-300` | existing scale | primary accent in dark mode (links, focus rings, active nav) | + +Gem hues (`red`, `blue`, `green`, `purple`, `amber`, etc. — used for Badge/IconBox +variants) keep their existing Tailwind palette; dark mode uses the **alpha-tint +pattern** on top of them (see below), it does not introduce new gem tokens. + +## 4. Standard class replacements + +Use these as the default mapping when sweeping a page. Deviate only when a +component already has a documented reason not to (e.g. specimen-plate rule). + +| Light-mode class | Add this dark: variant | +|---|---| +| `bg-white` | `dark:bg-coffee-raised` (surfaces) or `dark:bg-coffee-page` (page-level full-bleed sections) | +| `bg-slate-50` | `dark:bg-coffee-raised2` (usually a hover/alt-row bg) — check context, occasionally `dark:bg-coffee-sunk` for recessed/code areas | +| `bg-slate-100` | `dark:bg-coffee-raised2` | +| `text-slate-900` | `dark:text-cream-primary` | +| `text-slate-700` / `text-slate-800` | `dark:text-cream-secondary` | +| `text-slate-600` | `dark:text-cream-secondary` | +| `text-slate-500` | `dark:text-cream-muted` | +| `border-slate-200` | `dark:border-coffee-border` | +| `border-slate-300` | `dark:border-coffee-border-strong` | +| `hover:bg-slate-50` / `hover:bg-slate-100` | `dark:hover:bg-coffee-raised2` | +| `hover:text-slate-900` | `dark:hover:text-cream-primary` | +| `focus-visible:ring-crystal-500` | add `dark:focus-visible:ring-crystal-400` alongside (keep the light ring too) | +| `text-crystal-700` (as a link/accent) | `dark:text-crystal-400`, hover `dark:hover:text-crystal-300` | +| `bg-crystal-700` (primary button) | `dark:bg-crystal-600 dark:hover:bg-crystal-500` (deliberately one step lighter, not `crystal-700`/`800`, for dark-surface contrast) | + +General rule: **add** `dark:` variants alongside existing light classes — never +replace/remove a light class. The light look must stay pixel-identical when +`data-theme` is absent or `"light"`. + +## 5. Badge / IconBox alpha-tint pattern + +For any small colored chip/icon container keyed to a semantic or gem hue, the +dark-mode form is always: + +``` +dark:bg-{hue}-400/10 dark:text-{hue}-300 dark:border dark:border-{hue}-400/20 +``` + +(IconBox omits the border — it's a plain tinted container, not a bordered +chip: `dark:bg-{hue}-400/10 dark:text-{hue}-300` only.) + +This applies uniformly to all semantic variants (success/warning/danger, the +gem hues ruby/sapphire/emerald/amethyst/topaz, and the crystal-system variants +cubic/hexagonal/trigonal/tetragonal/orthorhombic/monoclinic/triclinic). Do not +invent a different treatment per hue — swap `{hue}` and keep the pattern. + +`default`/`outline`/`slate`-style neutral badges use the coffee/cream surface +tokens instead (e.g. `dark:bg-coffee-raised2 dark:text-cream-secondary`), since +they aren't tied to a semantic hue. + +Reference implementations: `src/components/ui/Badge.tsx`, +`src/components/ui/DifficultyBadge.tsx`, `src/components/ui/IconBox.tsx`, and +their `.astro` mirrors in `src/components/ui-astro/`. + +## 6. Crystal-SVG "specimen plate" rule + +Any container that renders a crystal/mineral SVG or 3D view (`.crystal-svg-container` +and equivalent) **stays permanently light**, regardless of theme. Real +specimens are photographed/rendered against a neutral light background — +inverting it would misrepresent the material and break color-accuracy for +gemmological identification. The only dark-mode addition allowed there is a +subtle border so the plate doesn't look like a stray white rectangle floating +on a dark page: + +``` +dark:border dark:border-coffee-border +``` + +Do **not** add `dark:bg-*` to specimen-plate containers. This is a hard rule, +not a style preference — see `global.css` for the existing comment on +`.crystal-svg-container`. + +## 7. Shadow + border rule + +Light-mode shadows (`shadow-sm`, `shadow-md`, etc.) read as muddy smudges on +dark coffee surfaces and should not be relied on for elevation in dark mode. +Prefer a **border** to communicate edges/elevation in dark mode instead: + +- Card/panel elevation in dark mode = `dark:border-coffee-border` (already on + `.card`), optionally `dark:border-coffee-border-strong` for a more + prominent panel. +- Don't bother adding `dark:shadow-none` defensively unless a specific shadow + is visibly wrong in a manual check — most existing shadows are low-opacity + enough to pass, but if you see a smudge, drop the shadow class in dark mode + and lean on the border instead. +- Hover-elevation (`.card-hover`) uses a lighter-still raised background + (`dark:hover:bg-coffee-raised2`) plus an accent border tint + (`dark:hover:border-crystal-400/40`) rather than a bigger shadow. + +## 8. The one cream-inverse-band exception + +`src/components/layout/Footer.astro` is the **only** place in the site that +flips to a light cream surface (`cream-inverse`, `#f3eadd`) with dark +coffee-ink text in dark mode. This is a deliberate jewlarray.ch signature +echo. It must remain unique — do not reuse `cream-inverse` or the "light +surface embedded in a dark page" pattern anywhere else. If a future design +wants to echo it again, treat that as a design decision requiring explicit +spec sign-off, not a default sweep move. + +## 9. UI kit coverage (already done — reuse, don't re-style) + +All components in `src/components/ui/` (React) and `src/components/ui-astro/` +(Astro mirrors, used by ~20 pages) already carry full dark-mode variants for +every interactive state (hover/focus/active/disabled): +`Button`, `Card`, `Badge`, `DifficultyBadge`, `IconBox`, `Link`, `SectionHeader`, +`Table`/`DataTable`/`PropertyTable`/`PaginatedTable`, `SearchInput`. +`Container` intentionally carries no color classes and needs none. + +**When sweeping pages: prefer swapping raw Tailwind color classes for these +components outright** rather than hand-rolling dark: variants inline. If a +page uses raw `
` instead of ``, +consider migrating it to the shared component as part of the sweep — it's +less work than maintaining a one-off dark treatment. + +## 10. Known gaps (explicitly out of foundation scope) + +These are real gaps, not oversights — they need page/feature-specific work by +the sweep agents that own those surfaces: + +- **Monaco / CDL playground editor theme.** The spec calls for switching the + Monaco editor to a dark theme (`vs-dark`) when `data-theme="dark"`. This is + JS wiring inside the playground's editor-mounting code (likely reacting to + the `themechange` document event and calling + `monaco.editor.setTheme('vs-dark' | 'vs')`), not a CSS/token concern. Not + implemented — needs to be picked up by whoever sweeps `/playground`. +- **`.prose` / typography-plugin dark styling.** Learn articles and other + markdown-rendered content use `@tailwindcss/typography`'s `prose` classes, + which currently have no dark counterpart wired up (`prose-invert` is + available from the plugin but not configured, and the custom + `typography.extend` overrides in `tailwind.config.mjs` — list styling, + code blocks, etc. — have hardcoded light colors, e.g. the `ol > li::before` + counter badge and `ul > li` border colors). This needs its own + `dark:prose-invert` pass plus dark equivalents for the customized + `typography.extend.DEFAULT.css` / `.lg.css` overrides. Not implemented — + needs to be picked up by whoever sweeps `/learn`. + +## 11. Hard gates (do not violate) + +1. Light mode must remain **pixel-identical** when `data-theme` is absent or + `"light"`. Every change is additive (`dark:` variants only). +2. Never invert crystal/mineral specimen SVG plates (see §6). +3. `cream-inverse` / light-surface-in-dark-page pattern is used **exactly + once** (Footer) — see §8. +4. `gold` is only ever a link-hover underline decoration or blockquote/citation + accent — never body text color, never text-on-gold or gold-on-text without + dark ink (`coffee-sunk`) underneath. +5. Don't add a global CSS transition on color/background for the theme switch + — the toggle switches instantly. Only the toggle's own icon crossfade + (150ms, disabled under `prefers-reduced-motion`) animates. +6. Always add `dark:focus-visible:ring-crystal-400` (or the ring color already + in use) next to any existing `focus-visible:ring-*` — don't let focus rings + disappear or go invisible against dark surfaces. +7. Don't set `data-theme` or touch `localStorage.theme` directly from new + code — always go through `window.__setTheme`. + +## 12. File map (foundation layer) + +- `tailwind.config.mjs` — `darkMode` config, `coffee`/`cream`/`gold` tokens +- `src/layouts/BaseLayout.astro` — no-FOUC script, `__setTheme`, `themechange` +- `src/components/layout/ThemeToggle.astro` — the toggle button +- `src/components/layout/Header.astro` — toggle placement (desktop + mobile), dark nav +- `src/components/layout/Footer.astro` — cream-inverse-band (§8) +- `src/styles/global.css` — dark base layer (body, headings, `.btn`, `.card`, + `.input`, selection, scrollbars), `color-scheme` CSS, specimen-plate rule +- `src/components/ui/*.tsx` and `src/components/ui-astro/*.astro` — UI kit + dark variants (§9) diff --git a/src/components/admin/AdminPanel.tsx b/src/components/admin/AdminPanel.tsx index f0709fd..c0a3d85 100644 --- a/src/components/admin/AdminPanel.tsx +++ b/src/components/admin/AdminPanel.tsx @@ -266,7 +266,7 @@ export function AdminPanel() {
-
{copiedCode && ( -
-

+

+

Code copied to clipboard: {copiedCode}

)} {error && ( -
-

{error}

+
+

{error}

)} @@ -334,18 +335,18 @@ export function AdminPanel() { {loading ? ( -
Loading...
+
Loading...
) : pendingRequests.length === 0 ? ( -
No pending requests
+
No pending requests
) : ( -
+
{pendingRequests.map((request) => (
-

{request.email}

-

{request.reason}

-

+

{request.email}

+

{request.reason}

+

Submitted: {formatDate(request.submitted)}

@@ -379,49 +380,49 @@ export function AdminPanel() { {loading ? ( -
Loading...
+
Loading...
) : codes.length === 0 ? ( -
No codes yet
+
No codes yet
) : (
- - + - - - - - + {codes.map((code) => ( - + - - + -
+
Code + Label + Uses + Created + Actions
{code.label} + {code.label} {code.uses} {code.maxUses !== null && ` / ${code.maxUses}`} + {formatDate(code.created)} @@ -452,7 +453,7 @@ export function AdminPanel() {
-
-
@@ -125,26 +125,26 @@ export function TreatmentWizard() { {selected.size === 0 ? ( -
+
Tick at least one clue above to see ranked treatments.
) : verdicts.length === 0 ? ( -
+
Selected clues do not point to any common treatment; the stone is likely natural or untreated within the limits of these observations.
) : (
-

+

{verdicts.length} candidate treatment{verdicts.length === 1 ? '' : 's'}

{verdicts.map((v) => (
-
{v.label}
+
{v.label}
@@ -153,16 +153,16 @@ export function TreatmentWizard() {
{v.supportingClueIds.length > 0 && ( -
- Supports:{' '} +
+ Supports:{' '} {v.supportingClueIds .map((id) => availableClues.find((c) => c.id === id)?.label ?? id) .join('; ')}
)} {v.contradictingClueIds.length > 0 && ( -
- Argues against:{' '} +
+ Argues against:{' '} {v.contradictingClueIds .map((id) => availableClues.find((c) => c.id === id)?.label ?? id) .join('; ')} @@ -173,7 +173,7 @@ export function TreatmentWizard() {
)} -
+
Note: this wizard reasons over visual & instrumental clues only. Some treatments (e.g. beryllium lattice diffusion, low-temperature heating of pastel sapphire) require advanced spectroscopy (LIBS / FTIR / UV-Vis) for definitive detection. Consult a recognised gem diff --git a/src/components/calculator/BirefringenceCalc.tsx b/src/components/calculator/BirefringenceCalc.tsx index 6b25df9..b54b7e8 100644 --- a/src/components/calculator/BirefringenceCalc.tsx +++ b/src/components/calculator/BirefringenceCalc.tsx @@ -40,9 +40,9 @@ export function BirefringenceCalc() { return (
-
+

Enter the maximum and minimum refractive index values to calculate birefringence.

-

+

Formula: Birefringence = RI(max) − RI(min)

@@ -88,7 +88,7 @@ export function BirefringenceCalc() { /> )} -
+

Example (Quartz): 1.553 − 1.544 = 0.009 (Low)

Example (Zircon): 1.984 − 1.925 = 0.059 (Very High)

Note: Isotropic gems (cubic system) have no birefringence.

diff --git a/src/components/calculator/CaratEstimator.tsx b/src/components/calculator/CaratEstimator.tsx index 63436fd..bec4015 100644 --- a/src/components/calculator/CaratEstimator.tsx +++ b/src/components/calculator/CaratEstimator.tsx @@ -174,9 +174,9 @@ export function CaratEstimator() { return (
-
+

Enter stone dimensions to estimate carat weight.

-

+

Formula: Weight = L × W × D × SG × Shape Factor

@@ -267,7 +267,7 @@ export function CaratEstimator() { /> )} -
+

Note: These are estimates. Actual weight varies with exact proportions, symmetry, and cut quality. The girdle factor accounts for material carried in a thicker-than-medium girdle.

Example (1ct diamond): 6.5 × 6.5 × 4.0 mm, SG 3.52, Round, medium girdle = ~1.0 ct

diff --git a/src/components/calculator/CriticalAngleCalc.tsx b/src/components/calculator/CriticalAngleCalc.tsx index 005607d..c6ad7b1 100644 --- a/src/components/calculator/CriticalAngleCalc.tsx +++ b/src/components/calculator/CriticalAngleCalc.tsx @@ -27,9 +27,9 @@ export function CriticalAngleCalc() { return (
-
+

Enter the refractive index to calculate the critical angle for total internal reflection.

-

+

Formula: θc = arcsin(1 ÷ RI)

@@ -51,7 +51,7 @@ export function CriticalAngleCalc() { {/* Hint when no result and no error but has input */} {!result && !errors.ri && values.ri && ( -
+
Enter a valid RI value (≥ 1.0) to calculate the critical angle.
)} @@ -71,9 +71,9 @@ export function CriticalAngleCalc() { /> )} -
-

Why This Matters

-

+

+

Why This Matters

+

Light entering a gem at angles greater than the critical angle will be totally internally reflected back into the stone. A smaller critical angle means more light is reflected, creating more brilliance. This is why diamond (θc = 24.4°) appears more brilliant than @@ -81,7 +81,7 @@ export function CriticalAngleCalc() {

-
+

Diamond (RI 2.417): θc = 24.4° (excellent light return)

Corundum (RI 1.77): θc = 34.4° (good light return)

Quartz (RI 1.55): θc = 40.2° (moderate light return)

diff --git a/src/components/calculator/DensityEstimator.tsx b/src/components/calculator/DensityEstimator.tsx index 3e15bec..c1615fa 100644 --- a/src/components/calculator/DensityEstimator.tsx +++ b/src/components/calculator/DensityEstimator.tsx @@ -85,10 +85,10 @@ export function DensityEstimator() { return (
-

+

Calculate density (SG) for irregular or fragile stones using volume estimation methods.

-

+

Formula: Density = Weight ÷ Volume

@@ -186,7 +186,7 @@ export function DensityEstimator() { {result && ( -
+
Calculated Volume: {result.calculatedVolume.toFixed(3)} cm³
)} @@ -194,7 +194,7 @@ export function DensityEstimator() { )} {needsMoreInput && ( -
+
Enter weight and {method === 'geometric' ? 'all dimensions' : 'volume'} to calculate density.
)} @@ -209,9 +209,9 @@ export function DensityEstimator() { /> )} -
-

When to Use This Tool

-
    +
    +

    When to Use This Tool

    +
    • • Fragile or porous stones that can't be submerged
    • • Irregular rough specimens without standard shapes
    • • Quick field estimates when lab equipment isn't available
    • diff --git a/src/components/calculator/DispersionCalculator.tsx b/src/components/calculator/DispersionCalculator.tsx index a39900f..e44bfa9 100644 --- a/src/components/calculator/DispersionCalculator.tsx +++ b/src/components/calculator/DispersionCalculator.tsx @@ -97,10 +97,10 @@ export function DispersionCalculator() { return (
      setHasInitiated(true)}>
      -

      +

      Enter the refractive index at red (C-line, 656nm) and violet (F-line, 486nm) wavelengths to calculate dispersion.

      -

      +

      Formula: Dispersion = RI(violet) − RI(red)

      @@ -148,7 +148,7 @@ export function DispersionCalculator() { )}
      -

      Gem Dispersion Reference

      +

      Gem Dispersion Reference

      -
      -

      Why Dispersion Matters

      -

      +

      +

      Why Dispersion Matters

      +

      Dispersion measures how much a gem splits white light into spectral colours. Higher dispersion creates more "fire" (the rainbow flashes seen in a well-cut stone). Diamond's high dispersion (0.044) is why it shows exceptional fire, while quartz's low dispersion (0.013) produces minimal colour flashes.

      diff --git a/src/components/calculator/HannemanRI.tsx b/src/components/calculator/HannemanRI.tsx index b6a9964..6334500 100644 --- a/src/components/calculator/HannemanRI.tsx +++ b/src/components/calculator/HannemanRI.tsx @@ -97,7 +97,7 @@ export function HannemanRI() { return (
      setHasInitiated(true)}> -
      +

      For stones above the refractometer scale (RI {'>'} 1.81) or rough material with no polished facet. Place the stone in a drop of each liquid and compare relief, then @@ -105,7 +105,7 @@ export function HannemanRI() {

      -
      +
      {rows.map((row, i) => (
      @@ -127,61 +127,61 @@ export function HannemanRI() {
      {loading && ( -
      +
      Loading mineral database…
      )} {dbError && ( -
      +
      Database unavailable: {dbError}
      )} {usable.length === 0 ? ( -
      +
      Select at least one observation above to infer an RI band.
      ) : !band ? null : band.min > band.max ? ( -
      +
      Conflicting observations. {band.rationale} Re-test with the suspect liquid.
      ) : ( <> -
      -
      +
      +
      Inferred RI band: {band.min.toFixed(2)} – {band.max.toFixed(2)}
      -
      {band.rationale}
      +
      {band.rationale}
      {!loading && !dbError && (
      -

      +

      {matches.length} candidate {matches.length === 1 ? 'species' : 'species'}

      {matches.length === 0 ? ( -
      +
      No species match this RI band. Re-check observations or try wider liquids.
      ) : ( paginated.map((m) => (
      {m.mineral.name} -
      +
      RI {m.mineral.ri_min?.toFixed(3)} – {m.mineral.ri_max?.toFixed(3)}
      {m.mineral.optical_character && ( -
      +
      {m.mineral.optical_character}
      )} diff --git a/src/components/calculator/LengthConverter.tsx b/src/components/calculator/LengthConverter.tsx index cd1590c..3ecc55d 100644 --- a/src/components/calculator/LengthConverter.tsx +++ b/src/components/calculator/LengthConverter.tsx @@ -30,9 +30,9 @@ export function LengthConverter() { return (
      -
      +

      Enter a value in either field to convert between length units.

      -

      +

      1 inch = 25.4 mm

      @@ -74,7 +74,7 @@ export function LengthConverter() {
      -
      +

      Common stone sizes:

      • 6.5mm round ≈ 1 carat diamond

      • 7mm round ≈ 1.25 carat diamond

      diff --git a/src/components/calculator/MeasurementTools.tsx b/src/components/calculator/MeasurementTools.tsx index 9405be6..dab949f 100644 --- a/src/components/calculator/MeasurementTools.tsx +++ b/src/components/calculator/MeasurementTools.tsx @@ -45,7 +45,7 @@ export function MeasurementTools() { {label} @@ -133,26 +133,26 @@ export function MeasurementTools() { {/* Learn More section */} -
      -

      Learn More

      -
        +
        +

        Learn More

        +
        • - + Hydrostatic SG measurement technique
        • - + Refractometer use, double readings, and the over-the-limit case
        • - + Optical properties: RI, birefringence, dispersion, critical angle
        • - + Physical properties: SG, hardness, density
        • diff --git a/src/components/calculator/RICalculator.tsx b/src/components/calculator/RICalculator.tsx index 8c43f36..aec304f 100644 --- a/src/components/calculator/RICalculator.tsx +++ b/src/components/calculator/RICalculator.tsx @@ -93,7 +93,7 @@ export function RICalculator() { return (
          -
          +

          Enter an RI reading to find matching gemstones. Toggle Double reading to enter both shadow-edge readings (ω/ε or α/γ) and infer birefringence + optic character automatically.

          @@ -155,17 +155,17 @@ export function RICalculator() { )} {doubleReadingResult && ( -
          -
          Double-reading inference
          -
          +
          +
          Double-reading inference
          +
          RI {doubleReadingResult.ri1.toFixed(3)} – {doubleReadingResult.ri2.toFixed(3)}, birefringence {doubleReadingResult.birefringence.toFixed(3)}{' '} ({doubleReadingResult.classification})
          -
          +
          Optic character: {doubleReadingResult.character}. {doubleReadingResult.characterLabel}
          -
          +
          Matches below use the average RI ({doubleReadingResult.lookupRI.toFixed(3)}) ± {values.tolerance}.
          @@ -181,7 +181,7 @@ export function RICalculator() { layout="list" /> ) : ( -
          +
          No common gems found near RI {lookupTarget.toFixed(3)} (±{values.tolerance}). Try widening the tolerance or checking your readings.
          @@ -191,7 +191,7 @@ export function RICalculator() { {/* Reference table */}
          -

          Common Gem RI Reference

          +

          Common Gem RI Reference

          @@ -62,14 +62,14 @@ export function ResultCard({ onClick={handleCopy} className={cn( 'absolute top-2 right-2 p-1.5 rounded-md transition-colors', - 'text-slate-500 hover:text-slate-600 hover:bg-white/50', - 'focus:outline-none focus:ring-2 focus:ring-crystal-500 focus:ring-offset-2' + 'text-slate-500 dark:text-cream-muted hover:text-slate-600 dark:hover:text-cream-secondary hover:bg-white/50 dark:hover:bg-coffee-raised2/60', + 'focus:outline-none focus:ring-2 focus:ring-crystal-500 dark:focus:ring-crystal-400 focus:ring-offset-2' )} aria-label={copied ? 'Copied!' : 'Copy result'} title={copied ? 'Copied!' : 'Copy to clipboard'} > {copied ? ( - + ) : ( @@ -87,18 +87,18 @@ export function ResultCard({ {/* Label */} {label && ( -

          {label}

          +

          {label}

          )} {/* Main value */}

          - {value} - {unit && {unit}} + {value} + {unit && {unit}}

          {/* Description */} {description && ( -

          {description}

          +

          {description}

          )} {/* Additional content */} @@ -120,9 +120,9 @@ interface ResultInlineProps { export function ResultInline({ value, unit, label, className }: ResultInlineProps) { return ( - {label && {label}:} - {value} - {unit && {unit}} + {label && {label}:} + {value} + {unit && {unit}} ); } @@ -146,18 +146,18 @@ export function ResultGroup({ results, layout = 'horizontal', className }: Resul return (
          {results.map((result, index) => (
          -

          {result.label}

          +

          {result.label}

          - {result.value} + {result.value} {result.unit && ( - {result.unit} + {result.unit} )}

          diff --git a/src/components/calculator/SGCalculator.tsx b/src/components/calculator/SGCalculator.tsx index bf4a15d..c078896 100644 --- a/src/components/calculator/SGCalculator.tsx +++ b/src/components/calculator/SGCalculator.tsx @@ -107,9 +107,9 @@ export function SGCalculator() { return (
          -
          +

          Enter the weight of your stone in air and water to calculate its specific gravity.

          -

          +

          Formula: SG = Wair ÷ ((Wair − Wwater) ÷ ρwater(T))

          @@ -150,7 +150,7 @@ export function SGCalculator() {
          -
          +
          ρwater at {tempC} °C = {waterDensity.toFixed(5)} g/cm³
          @@ -173,7 +173,7 @@ export function SGCalculator() { )} -
          +

          Example (Diamond): 3.52g in air, 2.52g in water at 20 °C = SG 3.52

          Tip: Ensure the stone is fully submerged and free of air bubbles. Temperature correction matters most for low-SG materials (opal, amber, beryl).

          diff --git a/src/components/calculator/TemperatureConverter.tsx b/src/components/calculator/TemperatureConverter.tsx index 1a7ada5..2fb82a8 100644 --- a/src/components/calculator/TemperatureConverter.tsx +++ b/src/components/calculator/TemperatureConverter.tsx @@ -38,9 +38,9 @@ export function TemperatureConverter() { return (
          -
          +

          Enter a value in either field to convert between temperature units.

          -

          +

          Formula: °F = (°C × 9/5) + 32

          @@ -82,9 +82,9 @@ export function TemperatureConverter() {
          -
          -

          Heat Treatment Temperatures

          -
          +
          +

          Heat Treatment Temperatures

          +

          Corundum: 1200-1800°C (2192-3272°F)

          Tanzanite: 550-700°C (1022-1292°F)

          Aquamarine: 400-450°C (752-842°F)

          @@ -93,7 +93,7 @@ export function TemperatureConverter() {
          -
          +

          Note: Heat treatment temperatures are approximate and depend on specific conditions including atmosphere, duration, and starting material.

          diff --git a/src/components/calculator/ValidationMessage.tsx b/src/components/calculator/ValidationMessage.tsx index d9ce480..bdafb63 100644 --- a/src/components/calculator/ValidationMessage.tsx +++ b/src/components/calculator/ValidationMessage.tsx @@ -28,9 +28,9 @@ export function ValidationMessage({

          -

          +

          Enter a value in any field to convert between weight units.

          -

          +

          1 carat = 0.2 grams = 200 milligrams

          @@ -107,7 +107,7 @@ export function WeightConverter() {
          -
          +

          Common weights:

          • 1 carat engagement diamond ≈ 0.2g

          • 5 carat sapphire ≈ 1.0g

          diff --git a/src/components/calculator/results/ClassifiedResult.tsx b/src/components/calculator/results/ClassifiedResult.tsx index 092e29c..27b3fc6 100644 --- a/src/components/calculator/results/ClassifiedResult.tsx +++ b/src/components/calculator/results/ClassifiedResult.tsx @@ -36,11 +36,11 @@ interface ClassifiedResultProps { } const levelBadgeClasses: Record = { - 'none': 'bg-slate-100 text-slate-700 border-slate-200', - 'low': 'bg-emerald-100 text-emerald-700 border-emerald-200', - 'medium': 'bg-blue-100 text-blue-700 border-blue-200', - 'high': 'bg-amber-100 text-amber-700 border-amber-200', - 'very-high': 'bg-red-100 text-red-700 border-red-200', + 'none': 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-coffee-raised2 dark:text-cream-secondary dark:border-coffee-border', + 'low': 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-400/10 dark:text-emerald-300 dark:border-emerald-400/20', + 'medium': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-400/10 dark:text-blue-300 dark:border-blue-400/20', + 'high': 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-400/10 dark:text-amber-300 dark:border-amber-400/20', + 'very-high': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-400/10 dark:text-red-300 dark:border-red-400/20', }; /** diff --git a/src/components/calculator/results/GemMatchBadges.tsx b/src/components/calculator/results/GemMatchBadges.tsx index 07f7dee..2e31dcb 100644 --- a/src/components/calculator/results/GemMatchBadges.tsx +++ b/src/components/calculator/results/GemMatchBadges.tsx @@ -15,9 +15,9 @@ interface GemMatch { } const originBadgeStyles: Record = { - synthetic: 'bg-blue-100 text-blue-700 border-blue-200', - simulant: 'bg-amber-100 text-amber-700 border-amber-200', - composite: 'bg-slate-100 text-slate-600 border-slate-300', + synthetic: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-400/10 dark:text-blue-300 dark:border-blue-400/20', + simulant: 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-400/10 dark:text-amber-300 dark:border-amber-400/20', + composite: 'bg-slate-100 text-slate-600 border-slate-300 dark:bg-coffee-raised2 dark:text-cream-secondary dark:border-coffee-border-strong', }; interface GemMatchBadgesProps { @@ -44,41 +44,41 @@ export function GemMatchBadges({ const hiddenCount = matches.length - maxVisible; return ( -
          +
          {matches.length > 0 ? ( <> {label && ( -

          {label}:

          +

          {label}:

          )}
          {visibleMatches.map((gem) => ( {gem.name} {gem.origin && gem.origin !== 'natural' && ( {gem.origin} )} {gem.propertyValue && ( - ({gem.propertyValue}) + ({gem.propertyValue}) )} ))} {hiddenCount > 0 && ( - + +{hiddenCount} more )}
          ) : ( -

          {emptyMessage}

          +

          {emptyMessage}

          )}
          ); diff --git a/src/components/calculator/results/GemMatchCard.tsx b/src/components/calculator/results/GemMatchCard.tsx index b007f76..1a933b0 100644 --- a/src/components/calculator/results/GemMatchCard.tsx +++ b/src/components/calculator/results/GemMatchCard.tsx @@ -55,20 +55,20 @@ export function GemMatchCard({ return (
          {/* Gem name + origin badge */} -

          +

          {gem.name} {gem.origin && gem.origin !== 'natural' && ( {gem.origin} @@ -82,14 +82,14 @@ export function GemMatchCard({ key={prop.key} className={cn( 'text-center p-1 rounded', - prop.matched && 'bg-crystal-100' + prop.matched && 'bg-crystal-100 dark:bg-crystal-400/10' )} > -

          {prop.label}

          +

          {prop.label}

          {prop.value} diff --git a/src/components/calculator/results/GemMatchList.tsx b/src/components/calculator/results/GemMatchList.tsx index e54b7cc..fa10ab2 100644 --- a/src/components/calculator/results/GemMatchList.tsx +++ b/src/components/calculator/results/GemMatchList.tsx @@ -64,10 +64,10 @@ export function GemMatchList({ return (

          {label && ( -

          +

          {label} {gems.length > 0 && ( - + ({gems.length} {gems.length === 1 ? 'match' : 'matches'}) )} @@ -104,7 +104,7 @@ export function GemMatchList({ )} ) : ( -

          +

          {emptyMessage}

          )} diff --git a/src/components/calculator/results/MultiValueResult.tsx b/src/components/calculator/results/MultiValueResult.tsx index 57f78bc..49429a9 100644 --- a/src/components/calculator/results/MultiValueResult.tsx +++ b/src/components/calculator/results/MultiValueResult.tsx @@ -37,12 +37,12 @@ interface MultiValueResultProps { } const variantTextClasses: Record = { - crystal: { value: 'text-crystal-700', unit: 'text-crystal-700' }, - emerald: { value: 'text-emerald-700', unit: 'text-emerald-600' }, - sapphire: { value: 'text-blue-700', unit: 'text-blue-600' }, - ruby: { value: 'text-red-700', unit: 'text-red-600' }, - topaz: { value: 'text-amber-700', unit: 'text-amber-600' }, - neutral: { value: 'text-slate-700', unit: 'text-slate-600' }, + crystal: { value: 'text-crystal-700 dark:text-crystal-400', unit: 'text-crystal-700 dark:text-crystal-400' }, + emerald: { value: 'text-emerald-700 dark:text-emerald-300', unit: 'text-emerald-600 dark:text-emerald-300' }, + sapphire: { value: 'text-blue-700 dark:text-blue-300', unit: 'text-blue-600 dark:text-blue-300' }, + ruby: { value: 'text-red-700 dark:text-red-300', unit: 'text-red-600 dark:text-red-300' }, + topaz: { value: 'text-amber-700 dark:text-amber-300', unit: 'text-amber-600 dark:text-amber-300' }, + neutral: { value: 'text-slate-700 dark:text-cream-secondary', unit: 'text-slate-600 dark:text-cream-secondary' }, }; const layoutClasses: Record = { @@ -71,7 +71,7 @@ export function MultiValueResult({ return (
          -

          {result.label}

          +

          {result.label}

          = { - crystal: { value: 'text-crystal-700', unit: 'text-crystal-700' }, - emerald: { value: 'text-emerald-700', unit: 'text-emerald-600' }, - sapphire: { value: 'text-blue-700', unit: 'text-blue-600' }, - ruby: { value: 'text-red-700', unit: 'text-red-600' }, - topaz: { value: 'text-amber-700', unit: 'text-amber-600' }, - neutral: { value: 'text-slate-700', unit: 'text-slate-600' }, + crystal: { value: 'text-crystal-700 dark:text-crystal-400', unit: 'text-crystal-700 dark:text-crystal-400' }, + emerald: { value: 'text-emerald-700 dark:text-emerald-300', unit: 'text-emerald-600 dark:text-emerald-300' }, + sapphire: { value: 'text-blue-700 dark:text-blue-300', unit: 'text-blue-600 dark:text-blue-300' }, + ruby: { value: 'text-red-700 dark:text-red-300', unit: 'text-red-600 dark:text-red-300' }, + topaz: { value: 'text-amber-700 dark:text-amber-300', unit: 'text-amber-600 dark:text-amber-300' }, + neutral: { value: 'text-slate-700 dark:text-cream-secondary', unit: 'text-slate-600 dark:text-cream-secondary' }, }; export function NumberResult({ @@ -85,14 +85,14 @@ export function NumberResult({ onClick={handleCopy} className={cn( 'absolute top-2 right-2 p-1.5 rounded-md transition-colors', - 'text-slate-500 hover:text-slate-600 hover:bg-white/50', - 'focus:outline-none focus:ring-2 focus:ring-crystal-500 focus:ring-offset-2' + 'text-slate-500 dark:text-cream-muted hover:text-slate-600 dark:hover:text-cream-secondary hover:bg-white/50 dark:hover:bg-coffee-raised2/60', + 'focus:outline-none focus:ring-2 focus:ring-crystal-500 dark:focus:ring-crystal-400 focus:ring-offset-2' )} aria-label={copied ? 'Copied!' : 'Copy result'} title={copied ? 'Copied!' : 'Copy to clipboard'} > {copied ? ( - + ) : ( @@ -110,7 +110,7 @@ export function NumberResult({ {/* Label */} {label && ( -

          {label}

          +

          {label}

          )} {/* Main value with unit */} @@ -126,7 +126,7 @@ export function NumberResult({ {/* Description */} {description && ( -

          {description}

          +

          {description}

          )} {/* Additional content */} diff --git a/src/components/calculator/results/ResultContainer.tsx b/src/components/calculator/results/ResultContainer.tsx index e5ae6fc..da3573f 100644 --- a/src/components/calculator/results/ResultContainer.tsx +++ b/src/components/calculator/results/ResultContainer.tsx @@ -22,12 +22,12 @@ interface ResultContainerProps { } const variantClasses: Record = { - crystal: 'bg-crystal-50 border-crystal-200', - emerald: 'bg-emerald-50 border-emerald-200', - sapphire: 'bg-blue-50 border-blue-200', - ruby: 'bg-red-50 border-red-200', - topaz: 'bg-amber-50 border-amber-200', - neutral: 'bg-slate-50 border-slate-200', + crystal: 'bg-crystal-50 border-crystal-200 dark:bg-crystal-400/10 dark:border-crystal-400/20', + emerald: 'bg-emerald-50 border-emerald-200 dark:bg-emerald-400/10 dark:border-emerald-400/20', + sapphire: 'bg-blue-50 border-blue-200 dark:bg-blue-400/10 dark:border-blue-400/20', + ruby: 'bg-red-50 border-red-200 dark:bg-red-400/10 dark:border-red-400/20', + topaz: 'bg-amber-50 border-amber-200 dark:bg-amber-400/10 dark:border-amber-400/20', + neutral: 'bg-slate-50 border-slate-200 dark:bg-coffee-raised2 dark:border-coffee-border', }; export function ResultContainer({ diff --git a/src/components/cases/CaseCard.tsx b/src/components/cases/CaseCard.tsx index 32f71b7..1757c65 100644 --- a/src/components/cases/CaseCard.tsx +++ b/src/components/cases/CaseCard.tsx @@ -74,19 +74,19 @@ export function CaseCard({ caseSummary }: CaseCardProps) { -
          +
          ~{caseSummary.estimatedMinutes} min
          {isComplete && entry?.result && ( -

          +

          Best result: {entry.result.percentage}% (+{entry.result.efficiencyBonus} efficiency)

          )} {hasProgress && ( -

          In progress — resume where you left off.

          +

          In progress — resume where you left off.

          )} diff --git a/src/components/cases/CaseDebrief.tsx b/src/components/cases/CaseDebrief.tsx index 57dbfe9..65a5938 100644 --- a/src/components/cases/CaseDebrief.tsx +++ b/src/components/cases/CaseDebrief.tsx @@ -31,7 +31,7 @@ export function CaseDebrief({ caseData, result, onRestart }: CaseDebriefProps) {
          -
          +
          Score: {result.rawScore}/{result.maxScore} ({result.percentage}%) @@ -39,10 +39,10 @@ export function CaseDebrief({ caseData, result, onRestart }: CaseDebriefProps) { +{result.efficiencyBonus} efficiency bonus )}
          -

          {caseData.debrief.summary}

          +

          {caseData.debrief.summary}

          -

          Expert path

          -
            +

            Expert path

            +
              {caseData.debrief.expertPath.map((step, i) => (
            1. {step}
            2. ))} @@ -50,8 +50,8 @@ export function CaseDebrief({ caseData, result, onRestart }: CaseDebriefProps) { {caseData.debrief.furtherReading && caseData.debrief.furtherReading.length > 0 && ( <> -

              Further reading

              -
                +

                Further reading

                +
                  {caseData.debrief.furtherReading.map((item, i) => (
                • {item}
                • ))} @@ -60,7 +60,7 @@ export function CaseDebrief({ caseData, result, onRestart }: CaseDebriefProps) { )} {caseData.references && caseData.references.length > 0 && ( -

                  +

                  {caseData.references.map((ref) => ref.citation).join(' ')}

                  )} @@ -78,17 +78,17 @@ export function CaseDebrief({ caseData, result, onRestart }: CaseDebriefProps) { const option = step?.options.find((o) => o.id === decision.optionId); if (!step || !option) return null; return ( -
                • -

                  +

                • +

                  {i + 1}. {step.prompt}

                  -

                  +

                  Chosen: {option.text}{' '} +{decision.scoreAwarded} pts

                  -

                  {option.rationale}

                  +

                  {option.rationale}

                • ); })} diff --git a/src/components/cases/CaseIntro.tsx b/src/components/cases/CaseIntro.tsx index e62e7ef..91cb604 100644 --- a/src/components/cases/CaseIntro.tsx +++ b/src/components/cases/CaseIntro.tsx @@ -35,9 +35,9 @@ export function CaseIntro({ caseData, hasSavedProgress, onStart }: CaseIntroProp ~{caseData.estimatedMinutes} min · {caseData.steps.length} steps -

                  {caseData.backstory}

                  -

                  Specimen

                  -

                  {caseData.specimenSummary}

                  +

                  {caseData.backstory}

                  +

                  Specimen

                  +

                  {caseData.specimenSummary}

                  {caseData.conceptTags && caseData.conceptTags.length > 0 && (
                  {caseData.conceptTags.map((tag) => ( diff --git a/src/components/cases/CaseOptionList.tsx b/src/components/cases/CaseOptionList.tsx index bca5c9f..5fa8173 100644 --- a/src/components/cases/CaseOptionList.tsx +++ b/src/components/cases/CaseOptionList.tsx @@ -8,7 +8,9 @@ * actually chose gets an additional highlight ring so it stands out among * same-tier siblings. * - * Light-only: no `dark:` classes (site convention for new Study components). + * Dark mode: tier colours map to alpha-tinted dark variants (emerald-400/10, + * amber-400/10, red-400/10 etc.), matching the pattern established in + * src/components/quiz/AnswerOption.tsx. */ import { cn } from '../ui/cn'; @@ -67,17 +69,17 @@ export function CaseOptionList({ className={cn( 'w-full flex items-start gap-3 p-4 rounded-lg border-2 text-left', 'transform transition-all duration-200 ease-out', - 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-crystal-500', + 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-crystal-500 dark:focus:ring-crystal-400', // Pre-submit default / selected states. - !isSubmitted && !isSelected && 'border-slate-200 bg-white hover:border-crystal-300 hover:bg-crystal-50 active:scale-[0.98]', - !isSubmitted && isSelected && 'border-crystal-500 bg-crystal-50 scale-[1.01] shadow-md', + !isSubmitted && !isSelected && 'border-slate-200 bg-white hover:border-crystal-300 hover:bg-crystal-50 active:scale-[0.98] dark:border-coffee-border dark:bg-coffee-raised dark:hover:border-crystal-400/40 dark:hover:bg-coffee-raised2', + !isSubmitted && isSelected && 'border-crystal-500 bg-crystal-50 scale-[1.01] shadow-md dark:border-crystal-400 dark:bg-crystal-400/10', // Post-submit: every option reveals its tier, so the acceptable // middle state is never lost even when it wasn't the choice made. - isSubmitted && option.weight === 'optimal' && 'border-emerald-500 bg-emerald-50', - isSubmitted && option.weight === 'acceptable' && 'border-amber-500 bg-amber-50', - isSubmitted && option.weight === 'poor' && 'border-red-500 bg-red-50', + isSubmitted && option.weight === 'optimal' && 'border-emerald-500 bg-emerald-50 dark:border-emerald-400 dark:bg-emerald-400/10', + isSubmitted && option.weight === 'acceptable' && 'border-amber-500 bg-amber-50 dark:border-amber-400 dark:bg-amber-400/10', + isSubmitted && option.weight === 'poor' && 'border-red-500 bg-red-50 dark:border-red-400 dark:bg-red-400/10', // The chosen option gets an extra highlight ring. - isChosen && 'ring-2 ring-offset-2 ring-slate-900/60', + isChosen && 'ring-2 ring-offset-2 ring-slate-900/60 dark:ring-cream-primary/60', isSubmitted && 'cursor-not-allowed', !isSubmitted && 'cursor-pointer', )} @@ -85,11 +87,11 @@ export function CaseOptionList({ {label} @@ -99,14 +101,14 @@ export function CaseOptionList({ {option.text} - {isChosen && (your choice)} + {isChosen && (your choice)} {isSubmitted && ( diff --git a/src/components/cases/CaseProgressStrip.tsx b/src/components/cases/CaseProgressStrip.tsx index a4e01d3..d483e22 100644 --- a/src/components/cases/CaseProgressStrip.tsx +++ b/src/components/cases/CaseProgressStrip.tsx @@ -20,18 +20,18 @@ export function CaseProgressStrip({ current, total, runningScore, className }: C return (
                  -
                  +
                  - + Step {current + 1} of {total} - - Score: {runningScore} + + Score: {runningScore}
                  diff --git a/src/components/cases/CaseStepFeedback.tsx b/src/components/cases/CaseStepFeedback.tsx index 0dc2ea4..0e7197b 100644 --- a/src/components/cases/CaseStepFeedback.tsx +++ b/src/components/cases/CaseStepFeedback.tsx @@ -5,9 +5,10 @@ * RationalePanel only has a binary `correct` prop, which would render an * `acceptable`-weight choice as "Not quite" even though partial credit was * awarded. To keep the tri-state tier legible, this wrapper adds its own - * light-only tier badge line ("+N points · optimal/acceptable/poor") above - * the panel rather than modifying RationalePanel (which contains `dark:` - * classes that must not be touched or imitated here). + * tier badge line ("+N points · optimal/acceptable/poor") above the panel + * rather than modifying RationalePanel itself. The badge line uses the + * `Badge` UI primitive (already dark-ready) so no local `dark:` classes are + * needed here; RationalePanel handles its own dark styling internally. */ import { RationalePanel } from '../quiz/study/RationalePanel'; diff --git a/src/components/cases/CaseStepPanel.tsx b/src/components/cases/CaseStepPanel.tsx index 2ef0de5..8838e75 100644 --- a/src/components/cases/CaseStepPanel.tsx +++ b/src/components/cases/CaseStepPanel.tsx @@ -34,7 +34,7 @@ export function CaseStepPanel({ return ( -

                  {step.prompt}

                  +

                  {step.prompt}

                  No evidence gathered yet.

                  ; + return

                  No evidence gathered yet.

                  ; } return (
                    {evidence.map((item) => ( -
                  • +
                  • - {item.label} + {item.label} {item.kind}
                    -

                    {item.value}

                    - {item.detail &&

                    {item.detail}

                    } +

                    {item.value}

                    + {item.detail &&

                    {item.detail}

                    } {item.toolHref && ( Open tool → @@ -51,8 +51,8 @@ export function EvidenceNotebook({ evidence }: EvidenceNotebookProps) { return ( <> {/* Below lg: collapsible disclosure above the step panel. */} -
                    - +
                    + Evidence ({evidence.length})
                    @@ -62,8 +62,8 @@ export function EvidenceNotebook({ evidence }: EvidenceNotebookProps) { {/* lg and up: always-open sticky right rail. */}

          {MOHS_SCALE.map(level => ( - - - - + + + + ))} @@ -167,13 +167,13 @@ export function HardnessReference() { placeholder="Search gemstone..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-400" + className="w-full px-3 py-1.5 text-sm border border-slate-300 dark:border-coffee-border rounded-lg bg-white dark:bg-coffee-sunk text-slate-900 dark:text-cream-primary placeholder-slate-500 dark:placeholder-cream-muted focus:outline-none focus:ring-2 focus:ring-amber-400 dark:focus:ring-crystal-400/20 focus:border-amber-400 dark:focus:border-crystal-400" /> {dbAvailable && paginatedData && ( - + {paginatedData.pagination.total} gems )} {loading ? ( -
          Loading gem data...
          +
          Loading gem data...
          ) : ( -
          +
          {level.hardness}{level.mineral}{level.wearResistance}
          {level.hardness}{level.mineral}{level.wearResistance}
          - - - - - + + + + + {filteredGems.map((gem, i) => ( - - - + + + - + ))}
          GemHardnessWearNotes
          GemHardnessWearNotes
          {gem.name}{gem.hardness}
          {gem.name}{gem.hardness} {gem.wearability} {gem.notes}{gem.notes}
          {filteredGems.length === 0 && ( -

          No matches found.

          +

          No matches found.

          )}
          )} @@ -227,23 +227,23 @@ export function HardnessReference() { )}
          {/* Info strip */} -
          +
          Hardness = resistance to scratching Toughness = resistance to breaking (not the same thing) • Diamond is hard but has perfect cleavage; jade is softer but tougher
          -
          +
          Learn more about hardness, toughness, and gem durability diff --git a/src/components/identification/IdentificationMatchCard.tsx b/src/components/identification/IdentificationMatchCard.tsx index 54926e6..45343af 100644 --- a/src/components/identification/IdentificationMatchCard.tsx +++ b/src/components/identification/IdentificationMatchCard.tsx @@ -34,11 +34,11 @@ export function IdentificationMatchCard({ {/* Header */}
          -

          +

          {mineral.name}

          {mineral.system && ( -

          +

          {mineral.system} {mineral.point_group && ` · ${mineral.point_group}`}

          @@ -49,10 +49,10 @@ export function IdentificationMatchCard({
          {confidenceScore}%
          @@ -68,9 +68,9 @@ export function IdentificationMatchCard({ {matchedProperties.map(prop => ( - + 0 && (
          -

          +

          Property Comparison

          @@ -95,12 +95,12 @@ export function IdentificationMatchCard({ key={detail.property} className={cn( 'flex items-center justify-between text-sm rounded px-2 py-1', - detail.matched ? 'bg-white/80' : 'bg-white/40' + detail.matched ? 'bg-white/80 dark:bg-coffee-raised2/80' : 'bg-white/40 dark:bg-coffee-raised2/40' )} > {detail.matched ? ( - + ) : ( - + )} - + {detail.property} {detail.measured} - + {detail.expected} @@ -143,7 +143,7 @@ export function IdentificationMatchCard({ {/* Additional mineral info */} {mineral.chemistry && ( -

          +

          {mineral.chemistry}

          )} @@ -180,25 +180,25 @@ export function IdentificationMatchCardCompact({ return (
          -

          +

          {mineral.name}

          -

          +

          {matchedProperties.length} of {result.matchDetails.length} properties match

          {confidenceScore}%
          diff --git a/src/components/lab/ChelseaFilter.tsx b/src/components/lab/ChelseaFilter.tsx index 9beb92b..1aac9d3 100644 --- a/src/components/lab/ChelseaFilter.tsx +++ b/src/components/lab/ChelseaFilter.tsx @@ -106,33 +106,33 @@ export function ChelseaFilter() { return (
          -

          +

          The Chelsea filter transmits deep red and yellow-green light, filtering out other wavelengths. Chromium-bearing gems fluoresce red.

          -
          -
          -
          -
          -

          Short-wave (254 nm)

          +
          +

          Short-wave (254 nm)

          - +
          + {headers.map(header => ( - ))} - + {rows.map(row => ( - + {row.map((cell, i) => { const hasCite = citationIndex && /\{cite:/.test(cell); return ( -
          + {header}
          + {hasCite ? : cell} diff --git a/src/components/learn/PropertyList.astro b/src/components/learn/PropertyList.astro index c5ce16f..38db3a5 100644 --- a/src/components/learn/PropertyList.astro +++ b/src/components/learn/PropertyList.astro @@ -83,11 +83,11 @@ const processedItems = items.map((item) => {
          {processedItems.map(item => ( -
          +
          -
          - +
          +
          @@ -95,9 +95,9 @@ const processedItems = items.map((item) => {
          - {item.name} + {item.name} {item.value && !isCDL(item.value) && ( - + {item.value} )} @@ -106,8 +106,8 @@ const processedItems = items.map((item) => { {/* Description — rendered as HTML when cite markers are present */} {item.resolvedDesc && ( item.descIsHtml - ?

          - :

          {item.resolvedDesc}

          + ?

          + :

          {item.resolvedDesc}

          )} {/* Item-level citation cluster */} @@ -117,9 +117,10 @@ const processedItems = items.map((item) => {
          )} - {/* CDL value block */} + {/* CDL value block — already dark ("terminal" style); add a border so it + doesn't float unbounded against the coffee-raised card in dark mode. */} {item.value && isCDL(item.value) && ( - + {item.value} )} @@ -137,3 +138,15 @@ const processedItems = items.map((item) => {
          ))}
          + + diff --git a/src/components/learn/References.astro b/src/components/learn/References.astro index b0c9191..ddd2b70 100644 --- a/src/components/learn/References.astro +++ b/src/components/learn/References.astro @@ -25,28 +25,28 @@ const citations = [...citationIndex.values()].sort((a, b) => a.n - b.n); {citations.length > 0 && (

          References

          -
            +
              {citations.map(({ id, n, ref }) => (
            1. - + {n}. @@ -55,3 +55,16 @@ const citations = [...citationIndex.values()].sort((a, b) => a.n - b.n);
          )} + + diff --git a/src/components/learn/SectionRenderer.astro b/src/components/learn/SectionRenderer.astro index cda6341..8847f6e 100644 --- a/src/components/learn/SectionRenderer.astro +++ b/src/components/learn/SectionRenderer.astro @@ -142,7 +142,7 @@ const sectionCitationCluster = ---
          -

          +

          {section.title} {sectionCitationCluster && ( @@ -158,7 +158,13 @@ const sectionCitationCluster = )} {sectionContent && ( -
          +
          )} {section.crystal && ( @@ -204,13 +210,19 @@ const sectionCitationCluster = } else { // Render as a regular subsection with improved styling return ( -
          -
          -

          {sub.title}

          +
          +
          +

          {sub.title}

          {sub.renderedContent && ( -
          +
          )} {sub.items && } {sub.table && } @@ -220,3 +232,32 @@ const sectionCitationCluster = } })}

          + + diff --git a/src/components/learn/icons.ts b/src/components/learn/icons.ts index 0adcfe4..645528b 100644 --- a/src/components/learn/icons.ts +++ b/src/components/learn/icons.ts @@ -68,54 +68,59 @@ export const crystalSystemColors: Record = { + // Dark variants extend the site's alpha-tint pattern (normally used for + // small badge/chip surfaces) to this larger card surface - a deliberate + // judgment call flagged in the sweep report: `bgLight`/`border`/`accent` + // use the hue's alpha-tinted forms and `text` moves to the lighter -300 + // step for AA contrast against the dark card background. cubic: { bg: 'bg-amber-700', - bgLight: 'bg-amber-50', - border: 'border-amber-200', - text: 'text-amber-800', - accent: 'bg-amber-100', + bgLight: 'bg-amber-50 dark:bg-amber-400/5', + border: 'border-amber-200 dark:border-amber-400/20', + text: 'text-amber-800 dark:text-amber-300', + accent: 'bg-amber-100 dark:bg-amber-400/10', }, hexagonal: { bg: 'bg-cyan-700', - bgLight: 'bg-cyan-50', - border: 'border-cyan-200', - text: 'text-cyan-800', - accent: 'bg-cyan-100', + bgLight: 'bg-cyan-50 dark:bg-cyan-400/5', + border: 'border-cyan-200 dark:border-cyan-400/20', + text: 'text-cyan-800 dark:text-cyan-300', + accent: 'bg-cyan-100 dark:bg-cyan-400/10', }, trigonal: { bg: 'bg-violet-700', - bgLight: 'bg-violet-50', - border: 'border-violet-200', - text: 'text-violet-800', - accent: 'bg-violet-100', + bgLight: 'bg-violet-50 dark:bg-violet-400/5', + border: 'border-violet-200 dark:border-violet-400/20', + text: 'text-violet-800 dark:text-violet-300', + accent: 'bg-violet-100 dark:bg-violet-400/10', }, tetragonal: { bg: 'bg-lime-700', - bgLight: 'bg-lime-50', - border: 'border-lime-200', - text: 'text-lime-800', - accent: 'bg-lime-100', + bgLight: 'bg-lime-50 dark:bg-lime-400/5', + border: 'border-lime-200 dark:border-lime-400/20', + text: 'text-lime-800 dark:text-lime-300', + accent: 'bg-lime-100 dark:bg-lime-400/10', }, orthorhombic: { bg: 'bg-orange-700', - bgLight: 'bg-orange-50', - border: 'border-orange-200', - text: 'text-orange-800', - accent: 'bg-orange-100', + bgLight: 'bg-orange-50 dark:bg-orange-400/5', + border: 'border-orange-200 dark:border-orange-400/20', + text: 'text-orange-800 dark:text-orange-300', + accent: 'bg-orange-100 dark:bg-orange-400/10', }, monoclinic: { bg: 'bg-rose-700', - bgLight: 'bg-rose-50', - border: 'border-rose-200', - text: 'text-rose-800', - accent: 'bg-rose-100', + bgLight: 'bg-rose-50 dark:bg-rose-400/5', + border: 'border-rose-200 dark:border-rose-400/20', + text: 'text-rose-800 dark:text-rose-300', + accent: 'bg-rose-100 dark:bg-rose-400/10', }, triclinic: { bg: 'bg-teal-700', - bgLight: 'bg-teal-50', - border: 'border-teal-200', - text: 'text-teal-800', - accent: 'bg-teal-100', + bgLight: 'bg-teal-50 dark:bg-teal-400/5', + border: 'border-teal-200 dark:border-teal-400/20', + text: 'text-teal-800 dark:text-teal-300', + accent: 'bg-teal-100 dark:bg-teal-400/10', }, }; diff --git a/src/components/minerals/CounterpartsSection.astro b/src/components/minerals/CounterpartsSection.astro index e945674..51c7574 100644 --- a/src/components/minerals/CounterpartsSection.astro +++ b/src/components/minerals/CounterpartsSection.astro @@ -30,19 +30,19 @@ const familyName = mineral.name.replace(/\s*\([^)]*\)\s*$/, '').trim(); if (!hasCounterparts && !hasNaturalCounterpart) return; const originStyles: Record = { - synthetic: 'bg-blue-100 text-blue-700', - simulant: 'bg-amber-100 text-amber-700', - composite: 'bg-slate-100 text-slate-600', - natural: 'bg-emerald-100 text-emerald-700', + synthetic: 'bg-blue-100 text-blue-700 dark:bg-blue-400/10 dark:text-blue-300', + simulant: 'bg-amber-100 text-amber-700 dark:bg-amber-400/10 dark:text-amber-300', + composite: 'bg-slate-100 text-slate-600 dark:bg-coffee-raised2 dark:text-cream-secondary', + natural: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300', }; --- -
          -
          -

          +
          +
          +

          {isNatural ? 'Synthetics & Simulants' : 'Natural Counterpart'}

          -

          +

          {isNatural ? `Known synthetic and simulant versions of ${familyName}` : `The natural gem this ${mineral.origin} imitates` @@ -50,7 +50,7 @@ const originStyles: Record = {

          -
          +
          {/* For natural minerals: show synthetic/simulant counterparts */} {isNatural && counterparts.map((cp) => { const hasSvg = isValidSvg(cp.model_svg); @@ -60,9 +60,9 @@ const originStyles: Record = { return ( -
          +
          {hasSvg ? (
          = {
          - {cp.name} + {cp.name} {cp.origin}
          {cp.growth_method && ( -
          {formatLabel(cp.growth_method)}
          +
          {formatLabel(cp.growth_method)}
          )}
          - +
          @@ -102,9 +102,9 @@ const originStyles: Record = { return ( -
          + {isNatural && counterparts.length > 0 && ( -
          +
          View all synthetic gems diff --git a/src/components/minerals/CrystalStructureCard.astro b/src/components/minerals/CrystalStructureCard.astro index 1b3788e..efae089 100644 --- a/src/components/minerals/CrystalStructureCard.astro +++ b/src/components/minerals/CrystalStructureCard.astro @@ -19,19 +19,19 @@ function parseForms(cdl: string): string[] { const forms = mineral.cdl ? parseForms(mineral.cdl) : []; --- -
          -
          -

          Crystal Structure

          +
          +
          +

          Crystal Structure

          {mineral.cdl && (
          -
          @@ -40,18 +40,18 @@ const forms = mineral.cdl ? parseForms(mineral.cdl) : [];
          -
          {mineral.point_group && (
          -
          @@ -61,12 +61,12 @@ const forms = mineral.cdl ? parseForms(mineral.cdl) : []; {forms.length > 0 && (
          -