056c47581f
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that presents the 4-section PDP pilot batch back to Umar, compliance, and the board in an editorial format. Adds the full data layer that drives it: 5 source-backed per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated product FAQs, and the Matrixify-ready CSV exports for Lewis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
956 lines
27 KiB
Vue
956 lines
27 KiB
Vue
<template>
|
|
<div class="patterns">
|
|
<!-- HERO ------------------------------------------------------ -->
|
|
<section class="hero page">
|
|
<div class="hero-grid">
|
|
<div class="hero-text">
|
|
<span class="eyebrow">Section by section</span>
|
|
<h1 class="hero-title">
|
|
Pick the<br />
|
|
<em>pattern</em>
|
|
</h1>
|
|
<p class="hero-lede">
|
|
For each of the four new PDP sections, here are five Just Vitamins drafts
|
|
on the left and the references they were measured against on the right.
|
|
Choose the structural pattern you want carried across the catalogue.
|
|
</p>
|
|
<div class="hero-meta">
|
|
<span class="meta">5 pilots</span>
|
|
<span class="hero-dot"></span>
|
|
<span class="meta">15 references</span>
|
|
<span class="hero-dot"></span>
|
|
<span class="meta">QA 100/100</span>
|
|
</div>
|
|
</div>
|
|
<div class="hero-ornament">
|
|
<div class="ornament-stack">
|
|
<div class="ornament-rule"></div>
|
|
<div class="ornament-num">04</div>
|
|
<div class="ornament-label">sections in scope</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SECTION TABS --------------------------------------------- -->
|
|
<nav class="section-tabs page" aria-label="Section">
|
|
<div class="tabs-rule"></div>
|
|
<div class="tabs-row">
|
|
<button
|
|
v-for="(s, i) in SECTIONS"
|
|
:key="s.key"
|
|
class="section-tab"
|
|
:class="{ 'is-active': activeKey === s.key }"
|
|
@click="select(s.key)"
|
|
>
|
|
<span class="tab-num">{{ String(i + 1).padStart(2, '0') }}</span>
|
|
<span class="tab-name">{{ s.label }}</span>
|
|
<span class="tab-meta">
|
|
{{ jvCount(s.key) }} JV · {{ refCount(s.key) }} ref
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- ACTIVE SECTION SPREAD ------------------------------------ -->
|
|
<section v-if="active" class="spread page">
|
|
<header class="spread-header">
|
|
<div>
|
|
<span class="eyebrow">Section {{ activeIndex + 1 }} of 4</span>
|
|
<h2 class="spread-title">{{ active.label }}</h2>
|
|
<p class="spread-blurb">{{ active.blurb }}</p>
|
|
</div>
|
|
<div class="spread-meta">
|
|
<span class="meta">Recommended template</span>
|
|
<strong class="spread-rec">{{ recommendedFor(active.key) }}</strong>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="spread-rule"></div>
|
|
|
|
<div class="cols">
|
|
<!-- JV COLUMN ------------------------------------------ -->
|
|
<div class="col col-jv">
|
|
<div class="col-head">
|
|
<span class="col-num">A.</span>
|
|
<span class="col-title">Just Vitamins drafts</span>
|
|
<span class="col-rule"></span>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">Loading drafts…</div>
|
|
<article
|
|
v-else
|
|
v-for="pilot in pilots"
|
|
:key="pilot.sku"
|
|
class="specimen specimen-jv"
|
|
:class="{ 'is-focused': focusSku === pilot.sku }"
|
|
@click="focusSku = pilot.sku"
|
|
>
|
|
<header class="specimen-head">
|
|
<div>
|
|
<span class="meta">{{ pilot.sku }} · {{ pilot.category }}</span>
|
|
<h3 class="specimen-title">{{ pilot.name }}</h3>
|
|
</div>
|
|
<span class="pip sage" :title="`QA ${pilot.qa.overallScore}`"></span>
|
|
</header>
|
|
<div class="specimen-body">
|
|
<component :is="JvRenderers[active.key]" :pilot="pilot" />
|
|
</div>
|
|
<footer class="specimen-cite">
|
|
<span class="cite-label">Source</span>
|
|
<span class="cite">{{ shortCite(jvSource(pilot, active.key)) }}</span>
|
|
</footer>
|
|
</article>
|
|
</div>
|
|
|
|
<!-- REFERENCE COLUMN ------------------------------------ -->
|
|
<div class="col col-ref">
|
|
<div class="col-head">
|
|
<span class="col-num">B.</span>
|
|
<span class="col-title">Reference patterns</span>
|
|
<span class="col-rule"></span>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">Loading references…</div>
|
|
<article
|
|
v-else
|
|
v-for="comp in compsForActive"
|
|
:key="comp.handle"
|
|
class="specimen specimen-ref"
|
|
:class="{ 'is-recommended': isRecommended(comp.handle) }"
|
|
>
|
|
<header class="specimen-head">
|
|
<div>
|
|
<span class="meta">{{ comp.brand }} · {{ comp.category }}</span>
|
|
<h3 class="specimen-title specimen-ref-title">
|
|
{{ refTitle(comp, active.key) }}
|
|
</h3>
|
|
</div>
|
|
<span class="ref-tag" v-if="isRecommended(comp.handle)">
|
|
<em>our pick</em>
|
|
</span>
|
|
</header>
|
|
<div class="specimen-body">
|
|
<component :is="RefRenderers[active.key]" :extract="comp" />
|
|
</div>
|
|
<footer class="specimen-actions">
|
|
<a class="action-link" :href="comp.url" target="_blank" rel="noopener">
|
|
view source ↗
|
|
</a>
|
|
<button
|
|
class="action-link is-action"
|
|
@click="lockPattern(comp)"
|
|
>
|
|
use as template →
|
|
</button>
|
|
</footer>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- LOCK CONFIRMATION TOAST -->
|
|
<transition name="fade">
|
|
<div v-if="locked" class="locked-toast">
|
|
<span class="eyebrow">Pattern locked</span>
|
|
<strong>{{ locked.brand }} {{ locked.sectionLabel }}</strong>
|
|
<span class="locked-meta">applied to {{ active.label.toLowerCase() }}</span>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, h, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useEditorialApi } from '../composables/useEditorialApi.js'
|
|
|
|
const SECTIONS = [
|
|
{ key: 'scientificStudy', label: 'Scientific Studies', blurb: 'EFSA-authorised wording, peer-reviewed evidence, regulator-precise stat tiles.' },
|
|
{ key: 'timelineBlock', label: 'Results Timeline', blurb: 'Phase-by-phase narrative of what customers experience week-by-week or month-by-month, grounded in real reviews.' },
|
|
{ key: 'comparisonTable', label: 'Comparison Table', blurb: 'How Just Vitamins compares against the brands the customer is also weighing up.' },
|
|
{ key: 'faqBlock', label: 'FAQs', blurb: 'Three product-specific questions plus four standard pairs, in elite brand voice.' }
|
|
]
|
|
|
|
const RECOMMENDED = {
|
|
scientificStudy: { handle: 'nothing-fishy-collagen', brand: 'Nothing Fishy', sectionLabel: 'clinical citation depth' },
|
|
timelineBlock: { handle: 'heights-biotic', brand: 'Heights', sectionLabel: 'Month 1-4 cadence' },
|
|
comparisonTable: { handle: 'club-earlybird-brain-drops', brand: 'Club EarlyBird', sectionLabel: 'feature matrix' },
|
|
faqBlock: { handle: 'elavate-collagen', brand: 'Elavate', sectionLabel: 'depth of coverage' }
|
|
}
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const api = useEditorialApi()
|
|
const loading = ref(true)
|
|
const pilots = ref([])
|
|
const competitors = ref([])
|
|
const focusSku = ref(null)
|
|
const locked = ref(null)
|
|
|
|
const activeKey = computed(() => {
|
|
const r = route.params.section
|
|
return SECTIONS.find(s => s.key === r) ? r : SECTIONS[0].key
|
|
})
|
|
const active = computed(() => SECTIONS.find(s => s.key === activeKey.value))
|
|
const activeIndex = computed(() => SECTIONS.findIndex(s => s.key === activeKey.value))
|
|
|
|
function select(key) {
|
|
router.push({ name: 'patterns-section', params: { section: key } })
|
|
}
|
|
|
|
onMounted(async () => {
|
|
loading.value = true
|
|
try {
|
|
pilots.value = await api.loadAllPilots()
|
|
competitors.value = await api.loadAllCompetitors()
|
|
focusSku.value = pilots.value[0]?.sku || null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
// Counters
|
|
function jvCount(_key) { return pilots.value.length }
|
|
function refCount(key) {
|
|
return competitors.value.filter(c => sectionPresent(c, key)).length
|
|
}
|
|
function sectionPresent(comp, key) {
|
|
const map = {
|
|
scientificStudy: 'scientific_study',
|
|
timelineBlock: 'results_timeline',
|
|
comparisonTable: 'comparison_table',
|
|
faqBlock: 'faq_block'
|
|
}
|
|
return Boolean(comp?.sections?.[map[key]]?.present)
|
|
}
|
|
|
|
const compsForActive = computed(() => {
|
|
return competitors.value
|
|
.filter(c => sectionPresent(c, activeKey.value))
|
|
.sort((a, b) => Number(isRecommended(b.handle)) - Number(isRecommended(a.handle)))
|
|
})
|
|
|
|
function isRecommended(handle) {
|
|
return RECOMMENDED[activeKey.value]?.handle === handle
|
|
}
|
|
function recommendedFor(key) {
|
|
const r = RECOMMENDED[key]
|
|
return r ? `${r.brand} — ${r.sectionLabel}` : '—'
|
|
}
|
|
|
|
function lockPattern(comp) {
|
|
locked.value = {
|
|
brand: comp.brand,
|
|
sectionLabel: SECTIONS.find(s => s.key === activeKey.value)?.label
|
|
}
|
|
setTimeout(() => { locked.value = null }, 3200)
|
|
}
|
|
|
|
// ---------- Citation helpers ----------
|
|
function jvSource(pilot, key) {
|
|
const src = pilot[key]?.sources?.[0] || ''
|
|
return src
|
|
}
|
|
function shortCite(s) {
|
|
if (!s) return '—'
|
|
return s.replace('data/intelligence/', '…/').replace('data/sources/', '…/').slice(0, 80)
|
|
}
|
|
|
|
// ---------- Refs key mapping helpers ----------
|
|
function refTitle(comp, key) {
|
|
const map = {
|
|
scientificStudy: 'scientific_study',
|
|
timelineBlock: 'results_timeline',
|
|
comparisonTable: 'comparison_table',
|
|
faqBlock: 'faq_block'
|
|
}
|
|
const sec = comp.sections?.[map[key]] || {}
|
|
return sec.headline || sec.section_title || '—'
|
|
}
|
|
|
|
// ============================================================
|
|
// RENDERERS — JV and competitor versions of each section
|
|
// ============================================================
|
|
|
|
const JvRenderers = {
|
|
scientificStudy: {
|
|
props: ['pilot'],
|
|
setup(props) {
|
|
return () => {
|
|
const ss = props.pilot.scientificStudy
|
|
return h('div', { class: 'render render-sci' }, [
|
|
h('h4', { class: 'render-headline' }, ss.headline),
|
|
h('p', { class: 'render-body' }, ss.body_copy),
|
|
ss.stats?.length ? h('div', { class: 'stat-row' },
|
|
ss.stats.slice(0, 3).map(s =>
|
|
h('div', { class: 'stat-tile' }, [
|
|
h('div', { class: 'stat-value' }, s.value),
|
|
h('div', { class: 'stat-label' }, s.label)
|
|
])
|
|
)
|
|
) : null
|
|
])
|
|
}
|
|
}
|
|
},
|
|
timelineBlock: {
|
|
props: ['pilot'],
|
|
setup(props) {
|
|
return () => {
|
|
const tb = props.pilot.timelineBlock
|
|
return h('div', { class: 'render render-timeline' }, [
|
|
h('h4', { class: 'render-headline' }, tb.headline),
|
|
tb.subtitle ? h('div', { class: 'render-subtitle' }, tb.subtitle) : null,
|
|
h('div', { class: 'phase-grid' },
|
|
tb.phases?.slice(0, 4).map(p =>
|
|
h('div', { class: 'phase-card' }, [
|
|
h('div', { class: 'phase-label' }, p.phase_label),
|
|
h('div', { class: 'phase-body' }, p.body_copy)
|
|
])
|
|
)
|
|
)
|
|
])
|
|
}
|
|
}
|
|
},
|
|
comparisonTable: {
|
|
props: ['pilot'],
|
|
setup(props) {
|
|
return () => {
|
|
const ct = props.pilot.comparisonTable
|
|
const cols = ct.columns || []
|
|
const rows = (ct.rows || []).slice(0, 4)
|
|
return h('div', { class: 'render render-cmp' }, [
|
|
h('h4', { class: 'render-headline' }, ct.section_title),
|
|
h('table', { class: 'cmp-table' }, [
|
|
h('thead', null, h('tr', null,
|
|
[h('th', null, 'Feature'), ...cols.map(c =>
|
|
h('th', { class: { 'is-us': c.is_us } }, c.name)
|
|
)]
|
|
)),
|
|
h('tbody', null, rows.map(r =>
|
|
h('tr', null, [
|
|
h('td', { class: 'feat' }, r.feature_label),
|
|
...cols.map((_c, i) => {
|
|
const v = r.values?.[String(i)]?.value || ''
|
|
return h('td', null, v.slice(0, 60))
|
|
})
|
|
])
|
|
))
|
|
])
|
|
])
|
|
}
|
|
}
|
|
},
|
|
faqBlock: {
|
|
props: ['pilot'],
|
|
setup(props) {
|
|
return () => {
|
|
const fb = props.pilot.faqBlock
|
|
const pairs = (fb.pairs || []).slice(0, 3)
|
|
return h('div', { class: 'render render-faq' }, [
|
|
h('h4', { class: 'render-headline' }, fb.section_title),
|
|
h('dl', { class: 'faq-list' },
|
|
pairs.flatMap(p => [
|
|
h('dt', { class: 'faq-q' }, p.question),
|
|
h('dd', { class: 'faq-a' }, p.answer)
|
|
])
|
|
)
|
|
])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const RefRenderers = {
|
|
scientificStudy: {
|
|
props: ['extract'],
|
|
setup(props) {
|
|
return () => {
|
|
const s = props.extract.sections?.scientific_study || {}
|
|
return h('div', { class: 'render render-sci' }, [
|
|
s.body_copy ? h('p', { class: 'render-body' }, s.body_copy) : null,
|
|
s.stat_tiles?.length ? h('div', { class: 'stat-row' },
|
|
s.stat_tiles.slice(0, 3).map(t =>
|
|
h('div', { class: 'stat-tile stat-tile-quiet' }, [
|
|
h('div', { class: 'stat-value' }, t.value || '—'),
|
|
h('div', { class: 'stat-label' }, t.label)
|
|
])
|
|
)
|
|
) : null,
|
|
s.citations?.length ? h('div', { class: 'cite-row' }, [
|
|
h('span', { class: 'meta' }, `${s.citations.length} citation${s.citations.length === 1 ? '' : 's'}`)
|
|
]) : null
|
|
])
|
|
}
|
|
}
|
|
},
|
|
timelineBlock: {
|
|
props: ['extract'],
|
|
setup(props) {
|
|
return () => {
|
|
const t = props.extract.sections?.results_timeline || {}
|
|
return h('div', { class: 'render render-timeline' }, [
|
|
t.intro_copy ? h('div', { class: 'render-subtitle' }, t.intro_copy) : null,
|
|
h('div', { class: 'phase-grid' },
|
|
(t.phases || []).slice(0, 4).map(p =>
|
|
h('div', { class: 'phase-card phase-card-quiet' }, [
|
|
h('div', { class: 'phase-label' }, p.phase_label),
|
|
h('div', { class: 'phase-body' }, p.body_copy || '')
|
|
])
|
|
)
|
|
)
|
|
])
|
|
}
|
|
}
|
|
},
|
|
comparisonTable: {
|
|
props: ['extract'],
|
|
setup(props) {
|
|
return () => {
|
|
const c = props.extract.sections?.comparison_table || {}
|
|
const headers = c.column_headers || []
|
|
const rows = (c.rows || []).slice(0, 4)
|
|
if (!headers.length) return h('div', { class: 'render-body' }, c.notes || 'Comparison present but not extractable as table.')
|
|
return h('div', { class: 'render render-cmp' }, [
|
|
h('table', { class: 'cmp-table cmp-table-quiet' }, [
|
|
h('thead', null, h('tr', null, headers.map(hd => h('th', null, hd)))),
|
|
h('tbody', null, rows.map(r =>
|
|
h('tr', null, [
|
|
h('td', { class: 'feat' }, r.feature_label),
|
|
h('td', null, (r.us_value || '').slice(0, 60)),
|
|
...(r.competitor_values || []).slice(0, 2).map(cv => h('td', null, (cv || '').slice(0, 60)))
|
|
])
|
|
))
|
|
])
|
|
])
|
|
}
|
|
}
|
|
},
|
|
faqBlock: {
|
|
props: ['extract'],
|
|
setup(props) {
|
|
return () => {
|
|
const f = props.extract.sections?.faq_block || {}
|
|
const pairs = (f.pairs || []).slice(0, 3)
|
|
return h('div', { class: 'render render-faq' }, [
|
|
h('dl', { class: 'faq-list faq-list-quiet' },
|
|
pairs.flatMap(p => [
|
|
h('dt', { class: 'faq-q' }, p.question),
|
|
h('dd', { class: 'faq-a' }, p.answer)
|
|
])
|
|
)
|
|
])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.patterns { padding-bottom: var(--s-9); }
|
|
|
|
/* HERO ----------------------------------------------------- */
|
|
|
|
.hero { padding-top: var(--s-7); padding-bottom: var(--s-7); }
|
|
.hero-grid {
|
|
display: grid;
|
|
grid-template-columns: 1.5fr 1fr;
|
|
gap: var(--s-8);
|
|
align-items: end;
|
|
}
|
|
|
|
.hero-title {
|
|
font-size: var(--t-5xl);
|
|
font-variation-settings: 'opsz' 144, 'SOFT' 80, 'WONK' 0;
|
|
line-height: 0.95;
|
|
letter-spacing: -0.04em;
|
|
margin-top: var(--s-3);
|
|
}
|
|
.hero-title em {
|
|
font-style: italic;
|
|
font-variation-settings: 'opsz' 144, 'SOFT' 100, 'WONK' 1;
|
|
color: var(--accent-sage);
|
|
}
|
|
|
|
.hero-lede {
|
|
margin-top: var(--s-5);
|
|
max-width: 56ch;
|
|
font-size: var(--t-lg);
|
|
line-height: var(--lh-relaxed);
|
|
color: var(--ink-soft);
|
|
}
|
|
|
|
.hero-meta {
|
|
margin-top: var(--s-6);
|
|
display: flex;
|
|
gap: var(--s-3);
|
|
align-items: center;
|
|
}
|
|
.hero-dot {
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: var(--ink-muted);
|
|
}
|
|
|
|
.hero-ornament {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: flex-end;
|
|
padding-bottom: var(--s-2);
|
|
}
|
|
.ornament-stack {
|
|
text-align: right;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--s-2);
|
|
align-items: flex-end;
|
|
}
|
|
.ornament-rule { width: 80px; height: 1px; background: var(--rule-strong); }
|
|
.ornament-num {
|
|
font-family: var(--font-display);
|
|
font-size: var(--t-6xl);
|
|
line-height: 0.8;
|
|
font-variation-settings: 'opsz' 144, 'SOFT' 100, 'WONK' 1, 'wght' 300;
|
|
color: var(--ink);
|
|
letter-spacing: -0.05em;
|
|
}
|
|
.ornament-label {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: var(--t-sm);
|
|
color: var(--ink-muted);
|
|
letter-spacing: 0.01em;
|
|
font-variation-settings: 'opsz' 24, 'WONK' 1;
|
|
}
|
|
|
|
/* TABS ----------------------------------------------------- */
|
|
|
|
.section-tabs { margin-top: var(--s-7); }
|
|
.tabs-rule { height: 1px; background: var(--rule); }
|
|
.tabs-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
border-bottom: 1px solid var(--rule);
|
|
}
|
|
|
|
.section-tab {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--s-2);
|
|
padding: var(--s-4) var(--s-4);
|
|
text-align: left;
|
|
position: relative;
|
|
background: transparent;
|
|
transition: background var(--dur) var(--ease);
|
|
}
|
|
.section-tab:hover { background: var(--bg-cream-alt); }
|
|
|
|
.section-tab::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: -1px;
|
|
height: 2px;
|
|
background: var(--ink);
|
|
transform: scaleX(0);
|
|
transform-origin: left;
|
|
transition: transform var(--dur) var(--ease-out);
|
|
}
|
|
.section-tab.is-active::after { transform: scaleX(1); }
|
|
|
|
.tab-num {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--ink-muted);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
.tab-name {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: var(--t-xl);
|
|
font-variation-settings: 'opsz' 36, 'WONK' 1, 'SOFT' 20;
|
|
letter-spacing: -0.012em;
|
|
color: var(--ink);
|
|
}
|
|
.tab-meta {
|
|
font-size: var(--t-xs);
|
|
color: var(--ink-muted);
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* SPREAD --------------------------------------------------- */
|
|
|
|
.spread { padding-top: var(--s-7); }
|
|
.spread-header {
|
|
display: grid;
|
|
grid-template-columns: 1.5fr 1fr;
|
|
align-items: end;
|
|
gap: var(--s-6);
|
|
margin-bottom: var(--s-4);
|
|
}
|
|
.spread-title {
|
|
font-size: var(--t-4xl);
|
|
font-variation-settings: 'opsz' 96, 'SOFT' 40;
|
|
letter-spacing: -0.03em;
|
|
margin-top: var(--s-2);
|
|
}
|
|
.spread-blurb {
|
|
max-width: 60ch;
|
|
margin-top: var(--s-3);
|
|
color: var(--ink-soft);
|
|
}
|
|
.spread-meta {
|
|
text-align: right;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--s-1);
|
|
}
|
|
.spread-rec {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-variation-settings: 'opsz' 30, 'WONK' 1;
|
|
font-size: var(--t-lg);
|
|
color: var(--accent-sage);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.spread-rule {
|
|
height: 1px;
|
|
background: var(--rule-strong);
|
|
margin: var(--s-5) 0 var(--s-6);
|
|
}
|
|
|
|
.cols {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1.2fr;
|
|
gap: var(--s-7);
|
|
align-items: start;
|
|
}
|
|
|
|
.col-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--s-3);
|
|
margin-bottom: var(--s-5);
|
|
}
|
|
.col-num {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-variation-settings: 'opsz' 36, 'WONK' 1, 'SOFT' 30;
|
|
font-size: var(--t-2xl);
|
|
color: var(--ink);
|
|
line-height: 1;
|
|
}
|
|
.col-title {
|
|
font-family: var(--font-display);
|
|
font-size: var(--t-lg);
|
|
font-variation-settings: 'opsz' 30;
|
|
letter-spacing: -0.015em;
|
|
}
|
|
.col-rule {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--rule);
|
|
}
|
|
|
|
/* SPECIMEN ------------------------------------------------- */
|
|
|
|
.specimen {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--s-4);
|
|
padding: var(--s-5);
|
|
border: 1px solid var(--rule);
|
|
background: var(--bg-paper);
|
|
margin-bottom: var(--s-5);
|
|
transition: all var(--dur) var(--ease);
|
|
}
|
|
|
|
.specimen-jv {
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
.specimen-jv::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: transparent;
|
|
transition: background var(--dur) var(--ease);
|
|
}
|
|
.specimen-jv:hover {
|
|
border-color: var(--rule-strong);
|
|
}
|
|
.specimen-jv.is-focused::before { background: var(--ink); }
|
|
.specimen-jv.is-focused {
|
|
background: var(--bg-cream-alt);
|
|
border-color: var(--rule-strong);
|
|
}
|
|
|
|
.specimen-ref.is-recommended {
|
|
border-color: var(--accent-sage);
|
|
background: var(--bg-sage-soft);
|
|
background: linear-gradient(180deg, var(--bg-paper), var(--bg-sage-soft));
|
|
}
|
|
.specimen-ref.is-recommended .specimen-title {
|
|
color: var(--ink);
|
|
}
|
|
|
|
.specimen-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: var(--s-4);
|
|
}
|
|
.specimen-title {
|
|
font-size: var(--t-xl);
|
|
font-variation-settings: 'opsz' 36, 'SOFT' 20;
|
|
letter-spacing: -0.015em;
|
|
line-height: 1.15;
|
|
margin-top: var(--s-1);
|
|
}
|
|
.specimen-ref-title {
|
|
font-style: italic;
|
|
font-variation-settings: 'opsz' 36, 'WONK' 1, 'SOFT' 30;
|
|
color: var(--ink-soft);
|
|
}
|
|
|
|
.ref-tag {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: var(--t-sm);
|
|
color: var(--accent-sage);
|
|
border: 1px solid var(--accent-sage);
|
|
padding: 2px 10px;
|
|
border-radius: 999px;
|
|
font-variation-settings: 'opsz' 24, 'WONK' 1;
|
|
}
|
|
|
|
.specimen-body { font-size: var(--t-base); }
|
|
.specimen-cite {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--s-2);
|
|
border-top: 1px dashed var(--rule);
|
|
padding-top: var(--s-3);
|
|
}
|
|
.cite-label {
|
|
font-size: var(--t-xs);
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-muted);
|
|
}
|
|
|
|
.specimen-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-top: 1px dashed var(--rule);
|
|
padding-top: var(--s-3);
|
|
}
|
|
.action-link {
|
|
font-family: var(--font-sans);
|
|
font-size: var(--t-sm);
|
|
color: var(--ink-soft);
|
|
text-decoration: none;
|
|
letter-spacing: 0.01em;
|
|
transition: color var(--dur) var(--ease);
|
|
}
|
|
.action-link:hover { color: var(--ink); }
|
|
.action-link.is-action {
|
|
color: var(--accent-sage);
|
|
font-weight: 500;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
}
|
|
.action-link.is-action:hover { color: var(--ink); }
|
|
|
|
/* RENDERERS (shared subtle look) --------------------------- */
|
|
:deep(.render-headline) {
|
|
font-family: var(--font-display);
|
|
font-size: var(--t-lg);
|
|
font-variation-settings: 'opsz' 24, 'SOFT' 20;
|
|
letter-spacing: -0.012em;
|
|
margin-bottom: var(--s-2);
|
|
line-height: 1.2;
|
|
}
|
|
:deep(.render-body) {
|
|
font-size: var(--t-sm);
|
|
line-height: var(--lh-relaxed);
|
|
color: var(--ink-soft);
|
|
}
|
|
:deep(.render-subtitle) {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: var(--t-sm);
|
|
color: var(--ink-muted);
|
|
margin-bottom: var(--s-3);
|
|
font-variation-settings: 'opsz' 24, 'WONK' 1;
|
|
}
|
|
|
|
:deep(.stat-row) {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: var(--s-3);
|
|
margin-top: var(--s-4);
|
|
}
|
|
:deep(.stat-tile) {
|
|
border-top: 1px solid var(--ink);
|
|
padding-top: var(--s-2);
|
|
}
|
|
:deep(.stat-tile-quiet) { border-top-color: var(--rule); }
|
|
:deep(.stat-value) {
|
|
font-family: var(--font-display);
|
|
font-size: var(--t-xl);
|
|
font-variation-settings: 'opsz' 36, 'SOFT' 20;
|
|
letter-spacing: -0.02em;
|
|
line-height: 1;
|
|
}
|
|
:deep(.stat-label) {
|
|
font-size: 11px;
|
|
color: var(--ink-muted);
|
|
letter-spacing: 0.01em;
|
|
margin-top: var(--s-2);
|
|
line-height: 1.3;
|
|
}
|
|
|
|
:deep(.phase-grid) {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: var(--s-3);
|
|
}
|
|
:deep(.phase-card) {
|
|
padding: var(--s-3) var(--s-4);
|
|
background: var(--bg-cream-alt);
|
|
border-radius: var(--r-sm);
|
|
}
|
|
:deep(.phase-card-quiet) {
|
|
background: transparent;
|
|
border: 1px dashed var(--rule);
|
|
}
|
|
:deep(.phase-label) {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: var(--t-sm);
|
|
font-variation-settings: 'opsz' 24, 'WONK' 1;
|
|
color: var(--accent-sage);
|
|
margin-bottom: var(--s-2);
|
|
}
|
|
:deep(.phase-body) {
|
|
font-size: var(--t-xs);
|
|
line-height: 1.5;
|
|
color: var(--ink-soft);
|
|
}
|
|
|
|
:deep(.cmp-table) {
|
|
width: 100%;
|
|
font-size: var(--t-xs);
|
|
border-collapse: collapse;
|
|
margin-top: var(--s-3);
|
|
}
|
|
:deep(.cmp-table th),
|
|
:deep(.cmp-table td) {
|
|
text-align: left;
|
|
padding: 6px 8px;
|
|
border-bottom: 1px solid var(--rule-faint);
|
|
vertical-align: top;
|
|
letter-spacing: -0.005em;
|
|
}
|
|
:deep(.cmp-table th) {
|
|
font-family: var(--font-sans);
|
|
font-size: 10.5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--ink-muted);
|
|
font-weight: 600;
|
|
}
|
|
:deep(.cmp-table th.is-us) { color: var(--accent-sage); }
|
|
:deep(.cmp-table .feat) {
|
|
font-weight: 500;
|
|
color: var(--ink);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
:deep(.faq-list) { display: flex; flex-direction: column; gap: var(--s-3); margin-top: var(--s-3); }
|
|
:deep(.faq-q) {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: var(--t-sm);
|
|
font-variation-settings: 'opsz' 24, 'WONK' 1;
|
|
color: var(--ink);
|
|
}
|
|
:deep(.faq-a) {
|
|
font-size: var(--t-xs);
|
|
line-height: 1.55;
|
|
color: var(--ink-soft);
|
|
margin-left: var(--s-4);
|
|
border-left: 1px solid var(--rule);
|
|
padding-left: var(--s-3);
|
|
}
|
|
|
|
/* LOCK TOAST ----------------------------------------------- */
|
|
.locked-toast {
|
|
position: fixed;
|
|
bottom: var(--s-7);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: var(--bg-ink);
|
|
color: var(--ink-on-dark);
|
|
padding: var(--s-3) var(--s-5);
|
|
border-radius: 999px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--s-3);
|
|
box-shadow: var(--shadow-lift);
|
|
z-index: 100;
|
|
}
|
|
.locked-toast .eyebrow {
|
|
color: var(--accent-sage);
|
|
font-size: 11px;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
font-style: normal;
|
|
font-family: var(--font-sans);
|
|
font-weight: 500;
|
|
}
|
|
.locked-toast strong {
|
|
font-family: var(--font-display);
|
|
font-variation-settings: 'opsz' 30, 'SOFT' 20;
|
|
font-weight: 400;
|
|
font-size: var(--t-base);
|
|
color: var(--ink-on-dark);
|
|
}
|
|
.locked-meta {
|
|
font-size: var(--t-xs);
|
|
color: rgba(244, 239, 230, 0.6);
|
|
}
|
|
|
|
.loading {
|
|
padding: var(--s-6);
|
|
color: var(--ink-muted);
|
|
font-style: italic;
|
|
font-family: var(--font-display);
|
|
}
|
|
|
|
/* Responsive ------------------------------------------------ */
|
|
@media (max-width: 1100px) {
|
|
.cols { grid-template-columns: 1fr; gap: var(--s-6); }
|
|
}
|
|
@media (max-width: 900px) {
|
|
.hero-grid { grid-template-columns: 1fr; }
|
|
.hero-ornament { display: none; }
|
|
.tabs-row { grid-template-columns: repeat(2, 1fr); }
|
|
.spread-header { grid-template-columns: 1fr; }
|
|
.spread-meta { text-align: left; }
|
|
}
|
|
</style>
|