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>
1103 lines
32 KiB
Vue
1103 lines
32 KiB
Vue
<template>
|
||
<div class="theatre">
|
||
<!-- HEADER + CONTROLS ---------------------------------------- -->
|
||
<section class="head page">
|
||
<div class="head-grid">
|
||
<div class="head-left">
|
||
<span class="eyebrow">PDP Preview Theatre</span>
|
||
<h1 class="head-title">
|
||
See it<br />
|
||
<em>as it ships</em>
|
||
</h1>
|
||
<p class="head-lede">
|
||
The full PDP render for each pilot SKU — every section in elite voice,
|
||
citation-linked, compliance-clean. Switch the render skin to inspect the
|
||
same content through a competitor's typography & colour.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="head-right">
|
||
<div class="select-block">
|
||
<span class="meta">Product</span>
|
||
<div class="sku-chips">
|
||
<router-link
|
||
v-for="p in pilots"
|
||
:key="p.sku"
|
||
class="sku-chip"
|
||
:class="{ 'is-active': activeSku === p.sku }"
|
||
:to="`/preview/${p.sku}`"
|
||
>
|
||
<span class="chip-sku">{{ p.sku.replace('JV-', '') }}</span>
|
||
<span class="chip-name">{{ p.name }}</span>
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="select-block select-block--skin">
|
||
<span class="meta">Render as</span>
|
||
<div class="skin-row">
|
||
<button
|
||
v-for="skin in SKINS"
|
||
:key="skin.key"
|
||
class="skin-pill"
|
||
:class="{ 'is-active': activeSkin === skin.key }"
|
||
:style="{ '--pill': skin.swatch }"
|
||
@click="activeSkin = skin.key"
|
||
>
|
||
<span class="skin-swatch"></span>
|
||
{{ skin.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- PDP STAGE -------------------------------------------- -->
|
||
<section v-if="pilot" class="stage page">
|
||
<div class="stage-frame" :class="`skin-${activeSkin}`">
|
||
<!-- Faux browser chrome to feel "live" -->
|
||
<header class="stage-chrome">
|
||
<div class="chrome-dots">
|
||
<span></span><span></span><span></span>
|
||
</div>
|
||
<div class="chrome-url">
|
||
<span>justvitamins.co.uk/products/{{ pilot.handle || pilot.sku.toLowerCase() }}</span>
|
||
</div>
|
||
<div class="chrome-skin">Rendered as {{ activeSkinLabel }}</div>
|
||
</header>
|
||
|
||
<!-- Above-the-fold hero -->
|
||
<section class="atf">
|
||
<div class="atf-image">
|
||
<div class="image-card">
|
||
<span class="image-meta">{{ pilot.sku }}</span>
|
||
<span class="image-title">{{ truth?.title || pilot.name }}</span>
|
||
<span class="image-strength" v-if="truth?.strength">{{ truth.strength }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="atf-text">
|
||
<span class="breadcrumb-meta">Home / {{ catLabel(pilot.category) }} / {{ truth?.title || pilot.name }}</span>
|
||
<h2 class="product-name">{{ truth?.title || pilot.name }}</h2>
|
||
<ul class="atf-bullets">
|
||
<li v-for="b in truthBullets" :key="b">✓ {{ b }}</li>
|
||
</ul>
|
||
<div class="variants" v-if="truth?.variants?.length">
|
||
<span
|
||
v-for="v in truth.variants"
|
||
:key="v.sku"
|
||
class="variant-pill"
|
||
>
|
||
{{ v.option }} · £{{ v.price }}
|
||
</span>
|
||
</div>
|
||
<button class="cta-btn">Add to bag</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section 01 — Scientific Studies -->
|
||
<section class="pdp-section" id="sci">
|
||
<header class="section-header">
|
||
<span class="section-num">01</span>
|
||
<div>
|
||
<span class="section-eyebrow">Scientific Studies</span>
|
||
<h3 class="section-title">{{ pilot.scientificStudy.headline }}</h3>
|
||
</div>
|
||
<div class="section-actions">
|
||
<span class="approve-state" :class="approvedState('scientificStudy')">{{ approveLabel('scientificStudy') }}</span>
|
||
<button class="action-btn" @click="approve('scientificStudy')">Approve ✓</button>
|
||
<button class="action-btn is-quiet" @click="openCite('scientificStudy')">Cite ↗</button>
|
||
</div>
|
||
</header>
|
||
<div class="section-body sci-body">
|
||
<p class="sci-prose">{{ pilot.scientificStudy.body_copy }}</p>
|
||
<div class="stat-strip">
|
||
<div v-for="s in pilot.scientificStudy.stats?.slice(0, 3)" :key="s.label" class="stat">
|
||
<div class="stat-num">{{ s.value }}</div>
|
||
<div class="stat-label">{{ s.label }}</div>
|
||
</div>
|
||
</div>
|
||
<a class="study-link" :href="pilot.scientificStudy.link_url" target="_blank" rel="noopener">
|
||
↗ {{ pilot.scientificStudy.link_label }}
|
||
</a>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section 02 — Timeline -->
|
||
<section class="pdp-section" id="timeline">
|
||
<header class="section-header">
|
||
<span class="section-num">02</span>
|
||
<div>
|
||
<span class="section-eyebrow">{{ pilot.timelineBlock.subtitle || 'Your results timeline' }}</span>
|
||
<h3 class="section-title">{{ pilot.timelineBlock.headline }}</h3>
|
||
</div>
|
||
<div class="section-actions">
|
||
<span class="approve-state" :class="approvedState('timelineBlock')">{{ approveLabel('timelineBlock') }}</span>
|
||
<button class="action-btn" @click="approve('timelineBlock')">Approve ✓</button>
|
||
<button class="action-btn is-quiet" @click="openCite('timelineBlock')">Cite ↗</button>
|
||
</div>
|
||
</header>
|
||
<div class="section-body">
|
||
<p class="intro-copy">{{ pilot.timelineBlock.intro_copy }}</p>
|
||
<div class="timeline-grid">
|
||
<article
|
||
v-for="(p, i) in pilot.timelineBlock.phases"
|
||
:key="p.handle"
|
||
class="phase"
|
||
>
|
||
<div class="phase-marker">
|
||
<span class="phase-tick">{{ String(i + 1).padStart(2, '0') }}</span>
|
||
</div>
|
||
<div class="phase-content">
|
||
<div class="phase-label">{{ p.phase_label }}</div>
|
||
<p class="phase-body">{{ p.body_copy }}</p>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section 03 — Comparison -->
|
||
<section class="pdp-section" id="cmp">
|
||
<header class="section-header">
|
||
<span class="section-num">03</span>
|
||
<div>
|
||
<span class="section-eyebrow">How we compare</span>
|
||
<h3 class="section-title">{{ pilot.comparisonTable.section_title }}</h3>
|
||
</div>
|
||
<div class="section-actions">
|
||
<span class="approve-state" :class="approvedState('comparisonTable')">{{ approveLabel('comparisonTable') }}</span>
|
||
<button class="action-btn" @click="approve('comparisonTable')">Approve ✓</button>
|
||
<button class="action-btn is-quiet" @click="openCite('comparisonTable')">Cite ↗</button>
|
||
</div>
|
||
</header>
|
||
<div class="section-body">
|
||
<p v-if="pilot.comparisonTable.subtitle" class="intro-copy">{{ pilot.comparisonTable.subtitle }}</p>
|
||
<div class="cmp-wrap">
|
||
<table class="cmp">
|
||
<thead>
|
||
<tr>
|
||
<th class="feature-col">Feature</th>
|
||
<th v-for="c in pilot.comparisonTable.columns" :key="c.handle" :class="{ 'col-us': c.is_us }">
|
||
{{ c.name }}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="r in pilot.comparisonTable.rows" :key="r.handle">
|
||
<td class="feature">{{ r.feature_label }}</td>
|
||
<td
|
||
v-for="(c, i) in pilot.comparisonTable.columns"
|
||
:key="c.handle"
|
||
:class="{ 'cell-us': c.is_us }"
|
||
>
|
||
{{ valueAt(r, i) }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section 04 — FAQs -->
|
||
<section class="pdp-section" id="faq">
|
||
<header class="section-header">
|
||
<span class="section-num">04</span>
|
||
<div>
|
||
<span class="section-eyebrow">Get answers</span>
|
||
<h3 class="section-title">{{ pilot.faqBlock.section_title }}</h3>
|
||
</div>
|
||
<div class="section-actions">
|
||
<span class="approve-state" :class="approvedState('faqBlock')">{{ approveLabel('faqBlock') }}</span>
|
||
<button class="action-btn" @click="approve('faqBlock')">Approve ✓</button>
|
||
<button class="action-btn is-quiet" @click="openCite('faqBlock')">Cite ↗</button>
|
||
</div>
|
||
</header>
|
||
<div class="section-body">
|
||
<div class="faq-stack">
|
||
<details v-for="(p, i) in pilot.faqBlock.pairs" :key="p.handle" :open="i < 2">
|
||
<summary>
|
||
<span class="faq-num">{{ String(i + 1).padStart(2, '0') }}</span>
|
||
<span class="faq-question">{{ p.question }}</span>
|
||
<span class="faq-scope" :class="`scope-${p.scope}`">{{ p.scope }}</span>
|
||
</summary>
|
||
<div class="faq-answer">{{ p.answer }}</div>
|
||
</details>
|
||
</div>
|
||
<a class="faq-cta" href="#">{{ pilot.faqBlock.cta_label }} →</a>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- Approval summary footer ------------------------ -->
|
||
<aside class="approval-summary">
|
||
<div class="approval-summary-inner">
|
||
<div class="approval-stat">
|
||
<span class="approval-num">{{ approvedCount }}/4</span>
|
||
<span class="meta">sections approved on this SKU</span>
|
||
</div>
|
||
<div class="approval-state-list">
|
||
<span v-for="key in SECTION_KEYS" :key="key" class="state-chip" :class="approvedState(key)">
|
||
{{ shortLabel(key) }}
|
||
</span>
|
||
</div>
|
||
<button class="btn is-primary" :disabled="approvedCount < 4">
|
||
<span v-if="approvedCount < 4">Approve all to ship</span>
|
||
<span v-else>Ship to Lewis →</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
</section>
|
||
|
||
<!-- CITATION DRAWER -->
|
||
<transition name="slide-up">
|
||
<div v-if="citeOpen" class="cite-drawer">
|
||
<div class="cite-drawer-inner">
|
||
<header class="cite-head">
|
||
<span class="eyebrow">Citation chain</span>
|
||
<h3 class="cite-title">{{ citeTitle }}</h3>
|
||
<button class="cite-close" @click="citeOpen = false">close ×</button>
|
||
</header>
|
||
<ul class="cite-list">
|
||
<li v-for="(c, i) in citeSources" :key="i">
|
||
<span class="cite-num">{{ String(i + 1).padStart(2, '0') }}</span>
|
||
<span class="cite-mono">{{ c }}</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useEditorialApi } from '../composables/useEditorialApi.js'
|
||
|
||
const SKINS = [
|
||
{ key: 'jv', label: 'JV native', swatch: '#5C6B4A' },
|
||
{ key: 'wild', label: 'Wild Nutrition', swatch: '#A89A82' },
|
||
{ key: 'heights', label: 'Heights', swatch: '#1F2D4F' },
|
||
{ key: 'ritual', label: 'Ritual', swatch: '#F2A85B' },
|
||
{ key: 'fishy', label: 'Nothing Fishy', swatch: '#1A3C2A' }
|
||
]
|
||
|
||
const SECTION_KEYS = ['scientificStudy', 'timelineBlock', 'comparisonTable', 'faqBlock']
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const api = useEditorialApi()
|
||
|
||
const pilots = ref([])
|
||
const pilot = ref(null)
|
||
const truth = ref(null)
|
||
const activeSkin = ref('jv')
|
||
const approvals = ref({}) // per-SKU-per-section
|
||
const citeOpen = ref(false)
|
||
const citeKey = ref(null)
|
||
|
||
const activeSku = computed(() => route.params.sku || pilots.value[0]?.sku || 'JV-D1000')
|
||
const activeSkinLabel = computed(() => SKINS.find(s => s.key === activeSkin.value)?.label || '')
|
||
|
||
const truthBullets = computed(() => {
|
||
if (!truth.value) return []
|
||
const out = []
|
||
if (truth.value.format) out.push(`Format: ${truth.value.format}`)
|
||
if (truth.value.strength) out.push(`Strength: ${truth.value.strength}`)
|
||
if (truth.value.tags) {
|
||
const dietary = truth.value.tags.split(',').map(t => t.trim()).filter(t => /vegan|vegetarian|gluten|kosher|halal/i.test(t))
|
||
if (dietary.length) out.push(`Suitable for: ${dietary.join(', ')}`)
|
||
}
|
||
if (truth.value.strapline) out.push(truth.value.strapline)
|
||
return out.slice(0, 4)
|
||
})
|
||
|
||
const approvedCount = computed(() => {
|
||
const sk = approvals.value[activeSku.value] || {}
|
||
return SECTION_KEYS.filter(k => sk[k]).length
|
||
})
|
||
|
||
function approve(key) {
|
||
const cur = approvals.value[activeSku.value] || {}
|
||
approvals.value = { ...approvals.value, [activeSku.value]: { ...cur, [key]: !cur[key] } }
|
||
}
|
||
function approvedState(key) {
|
||
const sk = approvals.value[activeSku.value] || {}
|
||
return sk[key] ? 'is-approved' : 'is-pending'
|
||
}
|
||
function approveLabel(key) {
|
||
const sk = approvals.value[activeSku.value] || {}
|
||
return sk[key] ? 'Approved' : 'Draft'
|
||
}
|
||
function shortLabel(key) {
|
||
const map = { scientificStudy: 'Sci', timelineBlock: 'Time', comparisonTable: 'Cmp', faqBlock: 'FAQ' }
|
||
return map[key]
|
||
}
|
||
|
||
function valueAt(row, i) {
|
||
const v = row.values?.[String(i)]?.value || ''
|
||
return v.length > 80 ? v.slice(0, 80) + '…' : v
|
||
}
|
||
|
||
function catLabel(c) {
|
||
return { vitamin: 'Vitamins', mineral: 'Minerals', botanical: 'Botanicals', collagen: 'Collagen' }[c] || c
|
||
}
|
||
|
||
// Citation drawer
|
||
const citeSources = computed(() => (pilot.value && citeKey.value) ? (pilot.value[citeKey.value]?.sources || []) : [])
|
||
const citeTitle = computed(() => {
|
||
if (!citeKey.value) return ''
|
||
const map = { scientificStudy: 'Scientific Studies', timelineBlock: 'Results Timeline', comparisonTable: 'Comparison Table', faqBlock: 'FAQs' }
|
||
return map[citeKey.value]
|
||
})
|
||
function openCite(key) {
|
||
citeKey.value = key
|
||
citeOpen.value = true
|
||
}
|
||
|
||
// Data loading
|
||
async function loadPilot(sku) {
|
||
pilot.value = null
|
||
truth.value = null
|
||
pilot.value = await api.loadSku(sku)
|
||
try { truth.value = await api.loadTruth(sku) } catch { truth.value = null }
|
||
}
|
||
|
||
onMounted(async () => {
|
||
pilots.value = await api.loadAllPilots()
|
||
if (!route.params.sku && pilots.value.length) {
|
||
router.replace(`/preview/${pilots.value[0].sku}`)
|
||
} else {
|
||
await loadPilot(activeSku.value)
|
||
}
|
||
})
|
||
|
||
watch(() => route.params.sku, async (sku) => {
|
||
if (sku) await loadPilot(sku)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.theatre { padding-bottom: var(--s-9); }
|
||
|
||
/* HEAD -------------------------------------------------------- */
|
||
.head { padding-top: var(--s-7); padding-bottom: var(--s-7); }
|
||
.head-grid {
|
||
display: grid;
|
||
grid-template-columns: 1.2fr 1fr;
|
||
gap: var(--s-8);
|
||
align-items: end;
|
||
}
|
||
.head-title {
|
||
font-size: var(--t-5xl);
|
||
font-variation-settings: 'opsz' 144, 'SOFT' 80;
|
||
line-height: 0.95;
|
||
letter-spacing: -0.04em;
|
||
margin-top: var(--s-3);
|
||
}
|
||
.head-title em {
|
||
font-style: italic;
|
||
font-variation-settings: 'opsz' 144, 'SOFT' 100, 'WONK' 1;
|
||
color: var(--accent-sage);
|
||
}
|
||
.head-lede {
|
||
margin-top: var(--s-5);
|
||
max-width: 56ch;
|
||
font-size: var(--t-lg);
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.head-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--s-5);
|
||
}
|
||
|
||
.select-block { display: flex; flex-direction: column; gap: var(--s-3); }
|
||
|
||
.sku-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--s-2);
|
||
}
|
||
.sku-chip {
|
||
border: 1px solid var(--rule);
|
||
background: var(--bg-paper);
|
||
padding: var(--s-3) var(--s-4);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
border-radius: var(--r-sm);
|
||
text-decoration: none;
|
||
transition: all var(--dur) var(--ease);
|
||
min-width: 140px;
|
||
}
|
||
.sku-chip:hover {
|
||
border-color: var(--rule-strong);
|
||
background: var(--bg-cream-alt);
|
||
}
|
||
.sku-chip.is-active {
|
||
border-color: var(--ink);
|
||
background: var(--ink);
|
||
color: var(--bg-paper);
|
||
}
|
||
.chip-sku {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--ink-muted);
|
||
}
|
||
.sku-chip.is-active .chip-sku { color: rgba(244, 239, 230, 0.55); }
|
||
.chip-name {
|
||
font-family: var(--font-display);
|
||
font-size: var(--t-sm);
|
||
font-variation-settings: 'opsz' 24, 'SOFT' 20;
|
||
line-height: 1.15;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
.skin-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }
|
||
.skin-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--s-2);
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--rule);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--t-xs);
|
||
letter-spacing: 0.02em;
|
||
color: var(--ink-soft);
|
||
background: transparent;
|
||
transition: all var(--dur) var(--ease);
|
||
}
|
||
.skin-pill:hover { border-color: var(--rule-strong); color: var(--ink); }
|
||
.skin-pill.is-active {
|
||
background: var(--ink);
|
||
color: var(--bg-paper);
|
||
border-color: var(--ink);
|
||
}
|
||
.skin-swatch {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--pill);
|
||
display: inline-block;
|
||
}
|
||
|
||
/* STAGE ------------------------------------------------------ */
|
||
.stage { padding-top: var(--s-5); }
|
||
|
||
.stage-frame {
|
||
border: 1px solid var(--rule-strong);
|
||
border-radius: var(--r-md);
|
||
background: var(--bg-paper);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-soft);
|
||
transition: background var(--dur-slow) var(--ease), color var(--dur-slow) var(--ease);
|
||
}
|
||
|
||
.stage-chrome {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--s-4);
|
||
padding: var(--s-3) var(--s-4);
|
||
background: var(--bg-cream-alt);
|
||
border-bottom: 1px solid var(--rule);
|
||
font-size: var(--t-xs);
|
||
color: var(--ink-muted);
|
||
}
|
||
.chrome-dots { display: flex; gap: 6px; }
|
||
.chrome-dots span {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: var(--rule-strong);
|
||
}
|
||
.chrome-url {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.chrome-skin {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-size: var(--t-xs);
|
||
letter-spacing: 0.01em;
|
||
font-variation-settings: 'opsz' 18, 'WONK' 1;
|
||
}
|
||
|
||
/* ABOVE-THE-FOLD --------------------------------------------- */
|
||
.atf {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: var(--s-7);
|
||
padding: var(--s-8) var(--s-7);
|
||
align-items: center;
|
||
}
|
||
.atf-image {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: var(--s-5);
|
||
}
|
||
.image-card {
|
||
width: 100%;
|
||
aspect-ratio: 3 / 4;
|
||
max-width: 320px;
|
||
background: var(--bg-cream-alt);
|
||
border: 1px solid var(--rule);
|
||
border-radius: var(--r-sm);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--s-2);
|
||
padding: var(--s-5);
|
||
text-align: center;
|
||
}
|
||
.image-meta {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.1em;
|
||
color: var(--ink-muted);
|
||
text-transform: uppercase;
|
||
}
|
||
.image-title {
|
||
font-family: var(--font-display);
|
||
font-size: var(--t-xl);
|
||
font-variation-settings: 'opsz' 36, 'SOFT' 30;
|
||
line-height: 1.1;
|
||
letter-spacing: -0.012em;
|
||
}
|
||
.image-strength {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-size: var(--t-lg);
|
||
color: var(--ink-soft);
|
||
font-variation-settings: 'opsz' 30, 'WONK' 1;
|
||
}
|
||
|
||
.breadcrumb-meta {
|
||
font-size: var(--t-xs);
|
||
color: var(--ink-muted);
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
}
|
||
.product-name {
|
||
font-size: var(--t-3xl);
|
||
font-variation-settings: 'opsz' 96, 'SOFT' 40;
|
||
letter-spacing: -0.03em;
|
||
margin-top: var(--s-2);
|
||
}
|
||
.atf-bullets {
|
||
margin-top: var(--s-5);
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--s-2);
|
||
}
|
||
.atf-bullets li {
|
||
font-size: var(--t-sm);
|
||
color: var(--ink-soft);
|
||
}
|
||
.variants {
|
||
margin-top: var(--s-5);
|
||
display: flex;
|
||
gap: var(--s-2);
|
||
}
|
||
.variant-pill {
|
||
padding: var(--s-2) var(--s-4);
|
||
border: 1px solid var(--rule-strong);
|
||
border-radius: var(--r-sm);
|
||
font-size: var(--t-sm);
|
||
font-family: var(--font-sans);
|
||
}
|
||
.cta-btn {
|
||
margin-top: var(--s-5);
|
||
padding: 14px 32px;
|
||
background: var(--ink);
|
||
color: var(--bg-paper);
|
||
border-radius: var(--r-sm);
|
||
font-family: var(--font-sans);
|
||
font-weight: 500;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
font-size: var(--t-sm);
|
||
cursor: pointer;
|
||
transition: background var(--dur) var(--ease);
|
||
}
|
||
.cta-btn:hover { background: var(--accent-sage); }
|
||
|
||
/* PDP SECTION ------------------------------------------------- */
|
||
.pdp-section {
|
||
border-top: 1px solid var(--rule);
|
||
padding: var(--s-8) var(--s-7);
|
||
}
|
||
|
||
.section-header {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
align-items: end;
|
||
gap: var(--s-5);
|
||
margin-bottom: var(--s-6);
|
||
}
|
||
|
||
.section-num {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-variation-settings: 'opsz' 96, 'WONK' 1, 'SOFT' 80, 'wght' 300;
|
||
font-size: var(--t-4xl);
|
||
line-height: 0.8;
|
||
color: var(--ink);
|
||
letter-spacing: -0.04em;
|
||
}
|
||
.section-eyebrow {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-variation-settings: 'opsz' 24, 'WONK' 1;
|
||
font-size: var(--t-sm);
|
||
color: var(--ink-muted);
|
||
letter-spacing: 0.01em;
|
||
display: block;
|
||
}
|
||
.section-title {
|
||
font-size: var(--t-2xl);
|
||
font-variation-settings: 'opsz' 60, 'SOFT' 30;
|
||
letter-spacing: -0.02em;
|
||
margin-top: var(--s-1);
|
||
line-height: 1.15;
|
||
}
|
||
|
||
.section-actions { display: flex; gap: var(--s-2); align-items: center; }
|
||
.approve-state {
|
||
font-family: var(--font-sans);
|
||
font-size: 10px;
|
||
padding: 4px 9px;
|
||
border-radius: 999px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
.approve-state.is-pending { background: var(--bg-cream-alt); color: var(--ink-muted); }
|
||
.approve-state.is-approved { background: var(--accent-sage); color: var(--bg-paper); }
|
||
|
||
.action-btn {
|
||
font-family: var(--font-sans);
|
||
font-size: var(--t-xs);
|
||
letter-spacing: 0.02em;
|
||
padding: 7px 14px;
|
||
border-radius: var(--r-sm);
|
||
border: 1px solid var(--ink);
|
||
background: var(--ink);
|
||
color: var(--bg-paper);
|
||
transition: all var(--dur) var(--ease);
|
||
}
|
||
.action-btn:hover { background: var(--accent-sage); border-color: var(--accent-sage); }
|
||
.action-btn.is-quiet {
|
||
background: transparent;
|
||
color: var(--ink);
|
||
border-color: var(--rule-strong);
|
||
}
|
||
.action-btn.is-quiet:hover {
|
||
background: var(--bg-cream-alt);
|
||
border-color: var(--ink);
|
||
color: var(--ink);
|
||
}
|
||
|
||
/* Section body */
|
||
.section-body { max-width: var(--col-wide); }
|
||
.sci-prose {
|
||
font-family: var(--font-display);
|
||
font-size: var(--t-xl);
|
||
font-variation-settings: 'opsz' 36, 'SOFT' 30;
|
||
line-height: var(--lh-snug);
|
||
letter-spacing: -0.012em;
|
||
color: var(--ink);
|
||
max-width: 60ch;
|
||
}
|
||
.intro-copy {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-variation-settings: 'opsz' 24, 'WONK' 1, 'SOFT' 20;
|
||
font-size: var(--t-lg);
|
||
color: var(--ink-soft);
|
||
max-width: 56ch;
|
||
margin-bottom: var(--s-6);
|
||
}
|
||
|
||
.stat-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: var(--s-7);
|
||
margin-top: var(--s-7);
|
||
padding-top: var(--s-5);
|
||
border-top: 1px solid var(--rule);
|
||
max-width: var(--col-mid);
|
||
}
|
||
.stat-num {
|
||
font-family: var(--font-display);
|
||
font-size: var(--t-4xl);
|
||
font-variation-settings: 'opsz' 96, 'SOFT' 60;
|
||
letter-spacing: -0.03em;
|
||
line-height: 1;
|
||
color: var(--accent-sage);
|
||
}
|
||
.stat-label {
|
||
font-size: 11px;
|
||
letter-spacing: 0.04em;
|
||
margin-top: var(--s-3);
|
||
color: var(--ink-muted);
|
||
text-transform: uppercase;
|
||
line-height: 1.4;
|
||
}
|
||
.study-link {
|
||
display: inline-block;
|
||
margin-top: var(--s-6);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--t-sm);
|
||
color: var(--ink-soft);
|
||
text-decoration: underline;
|
||
text-decoration-color: var(--accent-sage);
|
||
text-underline-offset: 4px;
|
||
}
|
||
|
||
/* Timeline */
|
||
.timeline-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 0;
|
||
border-top: 1px solid var(--rule);
|
||
}
|
||
.phase {
|
||
display: grid;
|
||
grid-template-columns: 80px 1fr;
|
||
gap: var(--s-5);
|
||
padding: var(--s-5) 0;
|
||
border-bottom: 1px solid var(--rule-faint);
|
||
align-items: start;
|
||
}
|
||
.phase-marker {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
}
|
||
.phase-tick {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
letter-spacing: 0.08em;
|
||
color: var(--accent-sage);
|
||
padding: 4px 8px;
|
||
border: 1px solid var(--accent-sage);
|
||
border-radius: var(--r-sm);
|
||
}
|
||
.phase-label {
|
||
font-family: var(--font-display);
|
||
font-variation-settings: 'opsz' 36, 'SOFT' 30;
|
||
font-size: var(--t-xl);
|
||
letter-spacing: -0.015em;
|
||
margin-bottom: var(--s-2);
|
||
}
|
||
.phase-body {
|
||
font-size: var(--t-base);
|
||
line-height: var(--lh-relaxed);
|
||
color: var(--ink-soft);
|
||
max-width: 64ch;
|
||
}
|
||
|
||
/* Comparison */
|
||
.cmp-wrap { overflow-x: auto; }
|
||
.cmp {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: var(--t-sm);
|
||
margin-top: var(--s-3);
|
||
}
|
||
.cmp th, .cmp td {
|
||
text-align: left;
|
||
padding: var(--s-3) var(--s-4);
|
||
border-bottom: 1px solid var(--rule);
|
||
vertical-align: top;
|
||
}
|
||
.cmp thead th {
|
||
font-family: var(--font-sans);
|
||
font-size: var(--t-xs);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--ink-muted);
|
||
font-weight: 600;
|
||
border-bottom: 2px solid var(--ink);
|
||
}
|
||
.cmp thead th.col-us { color: var(--accent-sage); }
|
||
.cmp td.feature {
|
||
font-weight: 500;
|
||
color: var(--ink);
|
||
}
|
||
.cmp td.cell-us {
|
||
background: var(--bg-sage-soft);
|
||
color: var(--ink);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* FAQ */
|
||
.faq-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-top: 1px solid var(--rule);
|
||
}
|
||
.faq-stack details {
|
||
border-bottom: 1px solid var(--rule);
|
||
padding: var(--s-5) 0;
|
||
}
|
||
.faq-stack summary {
|
||
list-style: none;
|
||
cursor: pointer;
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
align-items: center;
|
||
gap: var(--s-4);
|
||
}
|
||
.faq-stack summary::-webkit-details-marker { display: none; }
|
||
.faq-num {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--ink-muted);
|
||
}
|
||
.faq-question {
|
||
font-family: var(--font-display);
|
||
font-size: var(--t-lg);
|
||
font-variation-settings: 'opsz' 24, 'SOFT' 20;
|
||
letter-spacing: -0.012em;
|
||
color: var(--ink);
|
||
}
|
||
.faq-scope {
|
||
font-family: var(--font-sans);
|
||
font-size: 10px;
|
||
padding: 3px 8px;
|
||
border-radius: 999px;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
background: var(--bg-cream-alt);
|
||
color: var(--ink-muted);
|
||
}
|
||
.faq-scope.scope-product { background: var(--bg-sage-soft); color: var(--accent-sage); }
|
||
.faq-answer {
|
||
margin-top: var(--s-3);
|
||
font-size: var(--t-base);
|
||
line-height: var(--lh-relaxed);
|
||
color: var(--ink-soft);
|
||
max-width: 64ch;
|
||
padding-left: 28px;
|
||
}
|
||
.faq-cta {
|
||
display: inline-block;
|
||
margin-top: var(--s-5);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--t-sm);
|
||
color: var(--accent-sage);
|
||
text-decoration: none;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.faq-cta:hover { color: var(--ink); }
|
||
|
||
/* APPROVAL SUMMARY ---------------------------------------- */
|
||
.approval-summary {
|
||
margin-top: var(--s-7);
|
||
border: 1px solid var(--rule-strong);
|
||
background: var(--bg-paper);
|
||
border-radius: var(--r-md);
|
||
box-shadow: var(--shadow-soft);
|
||
}
|
||
.approval-summary-inner {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
gap: var(--s-5);
|
||
padding: var(--s-5) var(--s-6);
|
||
align-items: center;
|
||
}
|
||
.approval-num {
|
||
font-family: var(--font-display);
|
||
font-size: var(--t-3xl);
|
||
font-variation-settings: 'opsz' 96, 'SOFT' 60;
|
||
color: var(--ink);
|
||
display: block;
|
||
line-height: 1;
|
||
}
|
||
.approval-state-list { display: flex; gap: var(--s-2); flex-wrap: wrap; }
|
||
.state-chip {
|
||
font-family: var(--font-sans);
|
||
font-size: 11px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
letter-spacing: 0.02em;
|
||
background: var(--bg-cream-alt);
|
||
color: var(--ink-muted);
|
||
border: 1px solid var(--rule);
|
||
}
|
||
.state-chip.is-approved { background: var(--accent-sage); color: var(--bg-paper); border-color: var(--accent-sage); }
|
||
|
||
/* CITATION DRAWER ----------------------------------------- */
|
||
.cite-drawer {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 200;
|
||
background: var(--bg-ink);
|
||
color: var(--ink-on-dark);
|
||
border-top: 1px solid var(--rule-strong);
|
||
max-height: 50vh;
|
||
overflow-y: auto;
|
||
}
|
||
.cite-drawer-inner {
|
||
max-width: var(--col-full);
|
||
margin: 0 auto;
|
||
padding: var(--s-6) var(--s-7);
|
||
}
|
||
.cite-head {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
gap: var(--s-4);
|
||
align-items: center;
|
||
margin-bottom: var(--s-5);
|
||
}
|
||
.cite-head .eyebrow { color: var(--accent-sage); }
|
||
.cite-title {
|
||
font-size: var(--t-2xl);
|
||
color: var(--ink-on-dark);
|
||
font-variation-settings: 'opsz' 60;
|
||
}
|
||
.cite-close {
|
||
border: 1px solid rgba(244, 239, 230, 0.4);
|
||
color: var(--ink-on-dark);
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
font-size: var(--t-sm);
|
||
}
|
||
.cite-list {
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--s-3);
|
||
}
|
||
.cite-list li {
|
||
display: grid;
|
||
grid-template-columns: 36px 1fr;
|
||
gap: var(--s-3);
|
||
padding: var(--s-3) 0;
|
||
border-bottom: 1px solid rgba(244, 239, 230, 0.1);
|
||
}
|
||
.cite-num {
|
||
font-family: var(--font-mono);
|
||
color: rgba(244, 239, 230, 0.45);
|
||
font-size: var(--t-xs);
|
||
}
|
||
.cite-mono {
|
||
font-family: var(--font-mono);
|
||
font-size: var(--t-xs);
|
||
color: var(--ink-on-dark);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.slide-up-enter-active, .slide-up-leave-active { transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease); }
|
||
.slide-up-enter-from, .slide-up-leave-to { transform: translateY(100%); opacity: 0; }
|
||
|
||
/* ============================================================
|
||
* COMPETITOR SKIN OVERRIDES
|
||
* Same DOM, different visual language. Re-skin token by token.
|
||
* ============================================================ */
|
||
|
||
/* Wild Nutrition — warm taupe, more refined, Times-y serif */
|
||
.stage-frame.skin-wild {
|
||
--bg-paper: #FBF8F3;
|
||
--bg-cream-alt: #F1E8DB;
|
||
--bg-sage-soft: #E8E2D5;
|
||
--ink: #2A2622;
|
||
--ink-soft: #5A4F44;
|
||
--accent-sage: #948161;
|
||
--font-display: 'Fraunces', Georgia, serif;
|
||
background: #FBF8F3;
|
||
}
|
||
.stage-frame.skin-wild .stat-num { color: #948161; font-variation-settings: 'opsz' 96, 'wght' 300; }
|
||
.stage-frame.skin-wild .section-num { color: #948161; font-variation-settings: 'opsz' 144, 'wght' 200; }
|
||
.stage-frame.skin-wild .cta-btn { background: #2A2622; }
|
||
|
||
/* Heights — navy + cream, sans-heavy */
|
||
.stage-frame.skin-heights {
|
||
--bg-paper: #F3EFE4;
|
||
--bg-cream-alt: #E8E1D0;
|
||
--bg-sage-soft: #DBE2EB;
|
||
--ink: #1F2D4F;
|
||
--ink-soft: #4B5878;
|
||
--accent-sage: #1F2D4F;
|
||
--font-display: 'General Sans', sans-serif;
|
||
background: #F3EFE4;
|
||
}
|
||
.stage-frame.skin-heights .stat-num,
|
||
.stage-frame.skin-heights .section-num,
|
||
.stage-frame.skin-heights .product-name,
|
||
.stage-frame.skin-heights .section-title,
|
||
.stage-frame.skin-heights .phase-label,
|
||
.stage-frame.skin-heights .image-title,
|
||
.stage-frame.skin-heights .faq-question,
|
||
.stage-frame.skin-heights .sci-prose,
|
||
.stage-frame.skin-heights .head-title {
|
||
font-family: 'General Sans', sans-serif;
|
||
font-style: normal;
|
||
font-weight: 600;
|
||
font-variation-settings: normal;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
.stage-frame.skin-heights .intro-copy { font-family: 'General Sans', sans-serif; font-style: normal; font-variation-settings: normal; }
|
||
.stage-frame.skin-heights .cta-btn { background: #1F2D4F; }
|
||
|
||
/* Ritual — bright white, single brand-color accent */
|
||
.stage-frame.skin-ritual {
|
||
--bg-paper: #FFFFFF;
|
||
--bg-cream-alt: #F5F1EB;
|
||
--bg-sage-soft: #FFEED9;
|
||
--ink: #11243A;
|
||
--ink-soft: #475569;
|
||
--accent-sage: #F2A85B;
|
||
--rule: rgba(17, 36, 58, 0.12);
|
||
--rule-strong: rgba(17, 36, 58, 0.3);
|
||
background: #FFFFFF;
|
||
}
|
||
.stage-frame.skin-ritual .stat-num { color: #F2A85B; }
|
||
.stage-frame.skin-ritual .cta-btn { background: #11243A; }
|
||
.stage-frame.skin-ritual .cta-btn:hover { background: #F2A85B; color: #11243A; }
|
||
|
||
/* Nothing Fishy — deep green, modern */
|
||
.stage-frame.skin-fishy {
|
||
--bg-paper: #EFE9DD;
|
||
--bg-cream-alt: #DCD3C3;
|
||
--bg-sage-soft: #C8D3B8;
|
||
--ink: #1A3C2A;
|
||
--ink-soft: #406548;
|
||
--accent-sage: #1A3C2A;
|
||
background: #EFE9DD;
|
||
}
|
||
.stage-frame.skin-fishy .section-num { color: #1A3C2A; font-variation-settings: 'opsz' 144, 'wght' 500; }
|
||
.stage-frame.skin-fishy .stat-num { color: #1A3C2A; }
|
||
.stage-frame.skin-fishy .cta-btn { background: #1A3C2A; }
|
||
|
||
/* Responsive ------------------------------------------------ */
|
||
@media (max-width: 1100px) {
|
||
.atf { grid-template-columns: 1fr; }
|
||
.head-grid { grid-template-columns: 1fr; }
|
||
.section-header { grid-template-columns: 1fr; }
|
||
.section-actions { flex-wrap: wrap; }
|
||
.approval-summary-inner { grid-template-columns: 1fr; gap: var(--s-3); }
|
||
}
|
||
@media (max-width: 900px) {
|
||
.stat-strip { grid-template-columns: 1fr 1fr; gap: var(--s-4); }
|
||
.pdp-section { padding: var(--s-6) var(--s-5); }
|
||
.atf { padding: var(--s-5); }
|
||
.phase { grid-template-columns: 1fr; gap: var(--s-2); }
|
||
}
|
||
</style>
|