Files
justvitamin/src-editorial/views/PreviewTheatre.vue
T
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
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>
2026-06-02 18:50:09 +08:00

1103 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 &amp; 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>