feat: update desktop workflows and app center

This commit is contained in:
inman
2026-05-13 19:14:56 +08:00
parent 20b5aff4ad
commit 7c8781a6e3
160 changed files with 55492 additions and 1423 deletions

View File

@@ -0,0 +1,619 @@
/**
* <deck-stage> — reusable web component for HTML decks.
*
* Handles:
* (a) speaker notes — reads <script type="application/json" id="speaker-notes">
* and posts {slideIndexChanged: N} to the parent window on nav.
* (b) keyboard navigation — ←/→, PgUp/PgDn, Space, Home/End, number keys.
* (c) press R to reset to slide 0 (with a tasteful keyboard hint).
* (d) bottom-center overlay showing slide count + hints, fades out on idle.
* (e) auto-scaling — inner canvas is a fixed design size (default 1920×1080)
* scaled with `transform: scale()` to fit the viewport, letterboxed.
* Set the `noscale` attribute to render at authored size (1:1) — the
* PPTX exporter sets this so its DOM capture sees unscaled geometry.
* (f) print — `@media print` lays every slide out as its own page at the
* design size, so the browser's Print → Save as PDF produces a clean
* one-page-per-slide PDF with no extra setup.
*
* Slides are HIDDEN, not unmounted. Non-active slides stay in the DOM with
* `visibility: hidden` + `opacity: 0`, so their state (videos, iframes,
* form inputs, React trees) is preserved across navigation.
*
* Lifecycle event — the component dispatches a `slidechange` CustomEvent on
* itself whenever the active slide changes (including the initial mount).
* The event bubbles and composes out of shadow DOM, so you can listen on
* the <deck-stage> element or on document:
*
* document.querySelector('deck-stage').addEventListener('slidechange', (e) => {
* e.detail.index // new 0-based index
* e.detail.previousIndex // previous index, or -1 on init
* e.detail.total // total slide count
* e.detail.slide // the new active slide element
* e.detail.previousSlide // the prior slide element, or null on init
* e.detail.reason // 'init' | 'keyboard' | 'click' | 'tap' | 'api'
* });
*
* Persistence: none at the deck level. The host app keeps the current slide
* in its own URL (?slide=) and re-delivers it via location.hash on load, so a
* bare load with no hash always starts at slide 1.
*
* Usage:
* <deck-stage width="1920" height="1080">
* <section data-label="Title">...</section>
* <section data-label="Agenda">...</section>
* </deck-stage>
*
* Slides are the direct element children of <deck-stage>. Each slide is
* automatically tagged with:
* - data-screen-label="NN Label" (1-indexed, for comment flow)
* - data-om-validate="no_overflowing_text,no_overlapping_text,slide_sized_text"
*/
(() => {
const DESIGN_W_DEFAULT = 1920;
const DESIGN_H_DEFAULT = 1080;
const OVERLAY_HIDE_MS = 1800;
const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text';
const pad2 = (n) => String(n).padStart(2, '0');
const stylesheet = `
:host {
position: fixed;
inset: 0;
display: block;
background: #000;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
overflow: hidden;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.canvas {
position: relative;
transform-origin: center center;
flex-shrink: 0;
background: #fff;
will-change: transform;
}
/* Slides live in light DOM (via <slot>) so authored CSS still applies.
We absolutely position each slotted child to stack them. */
::slotted(*) {
position: absolute !important;
inset: 0 !important;
width: 100% !important;
height: 100% !important;
box-sizing: border-box !important;
overflow: hidden;
opacity: 0;
pointer-events: none;
visibility: hidden;
}
::slotted([data-deck-active]) {
opacity: 1;
pointer-events: auto;
visibility: visible;
}
/* Tap zones for mobile — back/forward thirds like Stories.
Transparent, no visible UI, don't block the overlay. */
.tapzones {
position: fixed;
inset: 0;
display: flex;
z-index: 2147482000;
pointer-events: none;
}
.tapzone {
flex: 1;
pointer-events: auto;
-webkit-tap-highlight-color: transparent;
}
/* Only activate tap zones on coarse pointers (touch devices). */
@media (hover: hover) and (pointer: fine) {
.tapzones { display: none; }
}
.overlay {
position: fixed;
left: 50%;
bottom: 22px;
transform: translate(-50%, 6px) scale(0.92);
filter: blur(6px);
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
background: #000;
color: #fff;
border-radius: 999px;
font-size: 12px;
font-feature-settings: "tnum" 1;
letter-spacing: 0.01em;
opacity: 0;
pointer-events: none;
transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease;
transform-origin: center bottom;
z-index: 2147483000;
user-select: none;
}
.overlay[data-visible] {
opacity: 1;
pointer-events: auto;
transform: translate(-50%, 0) scale(1);
filter: blur(0);
}
.btn {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
margin: 0;
padding: 0;
color: inherit;
font: inherit;
cursor: default;
display: inline-flex;
align-items: center;
justify-content: center;
height: 28px;
min-width: 28px;
border-radius: 999px;
color: rgba(255,255,255,0.72);
transition: background 140ms ease, color 140ms ease;
-webkit-tap-highlight-color: transparent;
}
.btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
.btn:active { background: rgba(255,255,255,0.18); }
.btn:focus { outline: none; }
.btn:focus-visible { outline: none; }
.btn::-moz-focus-inner { border: 0; }
.btn svg { width: 14px; height: 14px; display: block; }
.btn.reset {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
padding: 0 10px 0 12px;
gap: 6px;
color: rgba(255,255,255,0.72);
}
.btn.reset .kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 10px;
line-height: 1;
color: rgba(255,255,255,0.88);
background: rgba(255,255,255,0.12);
border-radius: 4px;
}
.count {
font-variant-numeric: tabular-nums;
color: #fff;
font-weight: 500;
padding: 0 8px;
min-width: 42px;
text-align: center;
font-size: 12px;
}
.count .sep { color: rgba(255,255,255,0.45); margin: 0 3px; font-weight: 400; }
.count .total { color: rgba(255,255,255,0.55); }
.divider {
width: 1px;
height: 14px;
background: rgba(255,255,255,0.18);
margin: 0 2px;
}
/* ── Print: one page per slide, no chrome ────────────────────────────
The screen layout stacks every slide at inset:0 inside a scaled
canvas; for print we want them in document flow at the authored
design size so the browser paginates one slide per sheet. The
@page size is set from the width/height attributes via the inline
<style id="deck-stage-print-page"> that connectedCallback injects
into <head> (the @page at-rule has no effect inside shadow DOM). */
@media print {
:host {
position: static;
inset: auto;
background: none;
overflow: visible;
color: inherit;
}
.stage { position: static; display: block; }
.canvas {
transform: none !important;
width: auto !important;
height: auto !important;
background: none;
will-change: auto;
}
::slotted(*) {
position: relative !important;
inset: auto !important;
width: var(--deck-design-w) !important;
height: var(--deck-design-h) !important;
box-sizing: border-box !important;
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto;
break-after: page;
page-break-after: always;
break-inside: avoid;
overflow: hidden;
}
::slotted(*:last-child) {
break-after: auto;
page-break-after: auto;
}
.overlay, .tapzones { display: none !important; }
}
`;
class DeckStage extends HTMLElement {
static get observedAttributes() { return ['width', 'height', 'noscale']; }
constructor() {
super();
this._root = this.attachShadow({ mode: 'open' });
this._index = 0;
this._slides = [];
this._notes = [];
this._hideTimer = null;
this._mouseIdleTimer = null;
this._onKey = this._onKey.bind(this);
this._onResize = this._onResize.bind(this);
this._onSlotChange = this._onSlotChange.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
this._onTapBack = this._onTapBack.bind(this);
this._onTapForward = this._onTapForward.bind(this);
}
get designWidth() {
return parseInt(this.getAttribute('width'), 10) || DESIGN_W_DEFAULT;
}
get designHeight() {
return parseInt(this.getAttribute('height'), 10) || DESIGN_H_DEFAULT;
}
connectedCallback() {
this._render();
this._loadNotes();
this._syncPrintPageRule();
window.addEventListener('keydown', this._onKey);
window.addEventListener('resize', this._onResize);
window.addEventListener('mousemove', this._onMouseMove, { passive: true });
// Initial collection + layout happens via slotchange, which fires on mount.
}
disconnectedCallback() {
window.removeEventListener('keydown', this._onKey);
window.removeEventListener('resize', this._onResize);
window.removeEventListener('mousemove', this._onMouseMove);
if (this._hideTimer) clearTimeout(this._hideTimer);
if (this._mouseIdleTimer) clearTimeout(this._mouseIdleTimer);
}
attributeChangedCallback() {
if (this._canvas) {
this._canvas.style.width = this.designWidth + 'px';
this._canvas.style.height = this.designHeight + 'px';
this._canvas.style.setProperty('--deck-design-w', this.designWidth + 'px');
this._canvas.style.setProperty('--deck-design-h', this.designHeight + 'px');
this._fit();
this._syncPrintPageRule();
}
}
_render() {
const style = document.createElement('style');
style.textContent = stylesheet;
const stage = document.createElement('div');
stage.className = 'stage';
const canvas = document.createElement('div');
canvas.className = 'canvas';
canvas.style.width = this.designWidth + 'px';
canvas.style.height = this.designHeight + 'px';
canvas.style.setProperty('--deck-design-w', this.designWidth + 'px');
canvas.style.setProperty('--deck-design-h', this.designHeight + 'px');
const slot = document.createElement('slot');
slot.addEventListener('slotchange', this._onSlotChange);
canvas.appendChild(slot);
stage.appendChild(canvas);
// Tap zones (mobile): left third = back, right third = forward.
const tapzones = document.createElement('div');
tapzones.className = 'tapzones export-hidden';
tapzones.setAttribute('aria-hidden', 'true');
tapzones.setAttribute('data-noncommentable', '');
const tzBack = document.createElement('div');
tzBack.className = 'tapzone tapzone--back';
const tzMid = document.createElement('div');
tzMid.className = 'tapzone tapzone--mid';
tzMid.style.pointerEvents = 'none';
const tzFwd = document.createElement('div');
tzFwd.className = 'tapzone tapzone--fwd';
tzBack.addEventListener('click', this._onTapBack);
tzFwd.addEventListener('click', this._onTapForward);
tapzones.append(tzBack, tzMid, tzFwd);
// Overlay: compact, solid black, with clickable controls.
const overlay = document.createElement('div');
overlay.className = 'overlay export-hidden';
overlay.setAttribute('role', 'toolbar');
overlay.setAttribute('aria-label', 'Deck controls');
overlay.setAttribute('data-noncommentable', '');
overlay.innerHTML = `
<button class="btn prev" type="button" aria-label="Previous slide" title="Previous (←)">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3L5 8l5 5"/></svg>
</button>
<span class="count" aria-live="polite"><span class="current">1</span><span class="sep">/</span><span class="total">1</span></span>
<button class="btn next" type="button" aria-label="Next slide" title="Next (→)">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
</button>
<span class="divider"></span>
<button class="btn reset" type="button" aria-label="Reset to first slide" title="Reset (R)">Reset<span class="kbd">R</span></button>
`;
overlay.querySelector('.prev').addEventListener('click', () => this._go(this._index - 1, 'click'));
overlay.querySelector('.next').addEventListener('click', () => this._go(this._index + 1, 'click'));
overlay.querySelector('.reset').addEventListener('click', () => this._go(0, 'click'));
this._root.append(style, stage, tapzones, overlay);
this._canvas = canvas;
this._slot = slot;
this._overlay = overlay;
this._countEl = overlay.querySelector('.current');
this._totalEl = overlay.querySelector('.total');
}
/** @page must live in the document stylesheet — it's a no-op inside
* shadow DOM. Inject/update a single <head> style tag so the print
* sheet matches the design size and Save-as-PDF yields one slide per
* page with no margins. */
_syncPrintPageRule() {
const id = 'deck-stage-print-page';
let tag = document.getElementById(id);
if (!tag) {
tag = document.createElement('style');
tag.id = id;
document.head.appendChild(tag);
}
tag.textContent =
'@page { size: ' + this.designWidth + 'px ' + this.designHeight + 'px; margin: 0; } ' +
'@media print { html, body { margin: 0 !important; padding: 0 !important; background: none !important; overflow: visible !important; height: auto !important; } ' +
'* { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }';
}
_onSlotChange() {
this._collectSlides();
this._restoreIndex();
this._applyIndex({ showOverlay: false, broadcast: true, reason: 'init' });
this._fit();
}
_collectSlides() {
const assigned = this._slot.assignedElements({ flatten: true });
this._slides = assigned.filter((el) => {
// Skip template/style/script nodes even if someone slots them.
const tag = el.tagName;
return tag !== 'TEMPLATE' && tag !== 'SCRIPT' && tag !== 'STYLE';
});
this._slides.forEach((slide, i) => {
const n = i + 1;
// Determine a label for comment flow: prefer explicit data-label,
// then an existing data-screen-label, then first heading, else "Slide".
let label = slide.getAttribute('data-label');
if (!label) {
const existing = slide.getAttribute('data-screen-label');
if (existing) {
// Strip any leading number the author may have included.
label = existing.replace(/^\s*\d+\s*/, '').trim() || existing;
}
}
if (!label) {
const h = slide.querySelector('h1, h2, h3, [data-title]');
if (h) label = (h.textContent || '').trim().slice(0, 40);
}
if (!label) label = 'Slide';
slide.setAttribute('data-screen-label', `${pad2(n)} ${label}`);
// Validation attribute for comment flow / auto-checks.
if (!slide.hasAttribute('data-om-validate')) {
slide.setAttribute('data-om-validate', VALIDATE_ATTR);
}
slide.setAttribute('data-deck-slide', String(i));
});
if (this._totalEl) this._totalEl.textContent = String(this._slides.length || 1);
if (this._index >= this._slides.length) this._index = Math.max(0, this._slides.length - 1);
}
_loadNotes() {
const tag = document.getElementById('speaker-notes');
if (!tag) { this._notes = []; return; }
try {
const parsed = JSON.parse(tag.textContent || '[]');
if (Array.isArray(parsed)) this._notes = parsed;
} catch (e) {
console.warn('[deck-stage] Failed to parse #speaker-notes JSON:', e);
this._notes = [];
}
}
_restoreIndex() {
// The host's ?slide= param is delivered as a #<int> hash (1-indexed) on
// the iframe src. No hash → slide 1; the deck itself keeps no position
// state across loads.
const h = (location.hash || '').match(/^#(\d+)$/);
if (h) {
const n = parseInt(h[1], 10) - 1;
if (n >= 0 && n < this._slides.length) this._index = n;
}
}
_applyIndex({ showOverlay = true, broadcast = true, reason = 'init' } = {}) {
if (!this._slides.length) return;
const prev = this._prevIndex == null ? -1 : this._prevIndex;
const curr = this._index;
// Keep the iframe's own hash in sync so an in-iframe location.reload()
// (reload banner path in viewer-handle.ts) lands on the current slide,
// not the stale deep-link hash from initial load.
try { history.replaceState(null, '', '#' + (curr + 1)); } catch (e) {}
this._slides.forEach((s, i) => {
if (i === curr) s.setAttribute('data-deck-active', '');
else s.removeAttribute('data-deck-active');
});
if (this._countEl) this._countEl.textContent = String(curr + 1);
if (broadcast) {
// (1) Legacy: host-window postMessage for speaker-notes renderers.
try { window.postMessage({ slideIndexChanged: curr }, '*'); } catch (e) {}
// (2) In-page CustomEvent on the <deck-stage> element itself.
// Bubbles and composes out of shadow DOM so slide code can listen:
// document.querySelector('deck-stage').addEventListener('slidechange', e => {
// e.detail.index, e.detail.previousIndex, e.detail.total, e.detail.slide, e.detail.reason
// });
const detail = {
index: curr,
previousIndex: prev,
total: this._slides.length,
slide: this._slides[curr] || null,
previousSlide: prev >= 0 ? (this._slides[prev] || null) : null,
reason: reason, // 'init' | 'keyboard' | 'click' | 'tap' | 'api'
};
this.dispatchEvent(new CustomEvent('slidechange', {
detail,
bubbles: true,
composed: true,
}));
}
this._prevIndex = curr;
if (showOverlay) this._flashOverlay();
}
_flashOverlay() {
if (!this._overlay) return;
this._overlay.setAttribute('data-visible', '');
if (this._hideTimer) clearTimeout(this._hideTimer);
this._hideTimer = setTimeout(() => {
this._overlay.removeAttribute('data-visible');
}, OVERLAY_HIDE_MS);
}
_fit() {
if (!this._canvas) return;
// PPTX export sets noscale so the DOM capture sees authored-size
// geometry — the scaled canvas is in shadow DOM, so the exporter's
// resetTransformSelector can't reach .canvas.style.transform directly.
if (this.hasAttribute('noscale')) {
this._canvas.style.transform = 'none';
return;
}
const vw = window.innerWidth;
const vh = window.innerHeight;
const s = Math.min(vw / this.designWidth, vh / this.designHeight);
this._canvas.style.transform = `scale(${s})`;
}
_onResize() { this._fit(); }
_onMouseMove() {
// Keep overlay visible while mouse moves; hide after idle.
this._flashOverlay();
}
_onTapBack(e) {
e.preventDefault();
this._go(this._index - 1, 'tap');
}
_onTapForward(e) {
e.preventDefault();
this._go(this._index + 1, 'tap');
}
_onKey(e) {
// Ignore when the user is typing.
const t = e.target;
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
const key = e.key;
let handled = true;
if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'Spacebar') {
this._go(this._index + 1, 'keyboard');
} else if (key === 'ArrowLeft' || key === 'PageUp') {
this._go(this._index - 1, 'keyboard');
} else if (key === 'Home') {
this._go(0, 'keyboard');
} else if (key === 'End') {
this._go(this._slides.length - 1, 'keyboard');
} else if (key === 'r' || key === 'R') {
this._go(0, 'keyboard');
} else if (/^[0-9]$/.test(key)) {
// 1..9 jump to that slide; 0 jumps to 10.
const n = key === '0' ? 9 : parseInt(key, 10) - 1;
if (n < this._slides.length) this._go(n, 'keyboard');
} else {
handled = false;
}
if (handled) {
e.preventDefault();
this._flashOverlay();
}
}
_go(i, reason = 'api') {
if (!this._slides.length) return;
const clamped = Math.max(0, Math.min(this._slides.length - 1, i));
if (clamped === this._index) {
this._flashOverlay();
return;
}
this._index = clamped;
this._applyIndex({ showOverlay: true, broadcast: true, reason });
}
// Public API ------------------------------------------------------------
/** Current slide index (0-based). */
get index() { return this._index; }
/** Total slide count. */
get length() { return this._slides.length; }
/** Programmatically navigate. */
goTo(i) { this._go(i, 'api'); }
next() { this._go(this._index + 1, 'api'); }
prev() { this._go(this._index - 1, 'api'); }
reset() { this._go(0, 'api'); }
}
if (!customElements.get('deck-stage')) {
customElements.define('deck-stage', DeckStage);
}
})();

View File

@@ -0,0 +1,658 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pink Script — After Hours</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="deck-stage.js"></script>
<style>
:root{
--ink: #060507;
--ink-2: #0F0D11;
--paper: #F5EDF1;
--pink: #ED3D8C;
--pink-2: #FF66A8;
--pink-deep: #B81D67;
--line: rgba(237,61,140,.32);
--mute: rgba(245,237,241,.55);
--hair: rgba(245,237,241,.14);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: #000; font-family: "Inter", system-ui, sans-serif; color: var(--paper); }
/* --- shared slide chrome ----------------------------------------- */
deck-stage > section.slide {
position: relative;
width: 1920px; height: 1080px;
background:
radial-gradient(ellipse 90% 70% at 30% 30%, #1A1218 0%, #0A0709 55%, #050306 100%);
color: var(--paper);
overflow: hidden;
}
/* film grain via tiny svg noise */
.slide::before {
content: ""; position: absolute; inset: 0; pointer-events: none;
opacity: .08; mix-blend-mode: screen;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}
/* hairline frame */
.slide::after {
content: ""; position: absolute; inset: 36px; pointer-events: none;
border: 1px solid var(--hair);
}
/* runner: top-left brand + top-right meta + bottom corners */
.runner {
position: absolute; left: 60px; right: 60px; top: 60px;
display: flex; align-items: baseline; justify-content: space-between;
font-family: "JetBrains Mono", monospace; font-size: 24px; letter-spacing: .14em;
text-transform: uppercase; color: var(--mute);
z-index: 5;
white-space: nowrap;
}
.runner .brand { color: var(--pink); }
.footer {
position: absolute; left: 60px; right: 60px; bottom: 60px;
display: flex; align-items: baseline; justify-content: space-between;
font-family: "JetBrains Mono", monospace; font-size: 24px; letter-spacing: .14em;
text-transform: uppercase; color: var(--mute);
z-index: 5;
white-space: nowrap;
}
.footer .pageno { color: var(--paper); white-space: nowrap; flex-shrink: 0; }
.footer .pageno em { color: var(--pink); font-style: normal; }
/* shared display script */
.script {
font-family: "Instrument Serif", serif;
font-style: italic;
font-weight: 400;
color: var(--pink);
letter-spacing: -.01em;
line-height: 1.05;
padding-bottom: .12em;
}
.script.huge { font-size: 540px; }
.script.giant { font-size: 360px; }
.script.large { font-size: 220px; }
.script.med { font-size: 140px; }
.script.sm { font-size: 88px; }
/* sans display */
.sans-display {
font-family: "Inter", sans-serif; font-weight: 300;
text-transform: uppercase; letter-spacing: .04em;
color: var(--paper);
}
.mono { font-family: "JetBrains Mono", monospace; letter-spacing: .12em; text-transform: uppercase; }
/* hairline pink rule */
.rule { height: 1px; background: var(--pink); opacity: .45; }
.rule.thin { opacity: .25; background: var(--paper); }
/* ============== 1. COVER · AFTER HOURS =================== */
.s-cover .stage {
position: absolute; left: 60px; right: 60px; top: 180px; bottom: 360px;
display: flex; align-items: center; justify-content: center;
flex-direction: column; gap: 20px;
}
.s-cover .pre { font-family: "JetBrains Mono", monospace; font-size: 28px; letter-spacing: .42em; text-transform: uppercase; color: var(--paper); opacity: .75; }
.s-cover .title-wrap { position: relative; }
.s-cover .title {
font-family: "Instrument Serif", serif; font-style: italic;
color: var(--pink); font-size: 280px; line-height: 1.02; letter-spacing: -.015em;
text-align: center;
text-shadow: 0 0 80px rgba(237,61,140,.18);
padding-bottom: .12em;
}
.s-cover .title .l2 { display: block; padding-left: 180px; color: var(--paper); }
.s-cover .sub {
font-family: "Inter", sans-serif; font-weight: 300; font-size: 28px; letter-spacing: .12em;
text-transform: uppercase; color: var(--paper); opacity: .85; margin-top: 28px;
}
.s-cover .sub em { color: var(--pink); font-style: normal; }
.s-cover .lower { position: absolute; left: 60px; right: 60px; bottom: 160px; display: flex; justify-content: space-between; align-items: end; gap: 32px; }
.s-cover .lower .col { display: flex; flex-direction: column; gap: 6px; }
.s-cover .lower .lab { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--mute); }
.s-cover .lower .val { font-family: "Instrument Serif", serif; font-style: italic; font-size: 48px; color: var(--pink); line-height: 1.05; }
.s-cover .lower .val.alt { color: var(--paper); }
/* ============== 2. THE INDEX (TOC) ======================= */
.s-toc .body { position: absolute; inset: 140px 60px 140px 60px; display: grid; grid-template-columns: 480px 1fr; gap: 80px; }
.s-toc h1 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 220px; line-height: 1.04; color: var(--pink); padding-bottom: .12em; }
.s-toc h1 .small { display: block; font-size: 80px; color: var(--paper); margin-top: 16px; opacity: .8; }
.s-toc .rows { display: flex; flex-direction: column; }
.s-toc .row { display: grid; grid-template-columns: 110px 1fr 200px; gap: 32px; align-items: baseline; padding: 26px 0; border-bottom: 1px solid var(--hair); }
.s-toc .row .num { font-family: "Instrument Serif", serif; font-style: italic; font-size: 64px; color: var(--pink); line-height: 1; }
.s-toc .row .title { font-family: "Instrument Serif", serif; font-style: italic; font-size: 56px; color: var(--paper); line-height: 1.05; }
.s-toc .row .desc { font-family: "Inter", sans-serif; font-size: 24px; color: var(--mute); margin-top: 8px; line-height: 1.4; font-style: normal; }
.s-toc .row .meta { font-family: "JetBrains Mono", monospace; font-size: 24px; letter-spacing: .12em; text-transform: uppercase; color: var(--mute); text-align: right; }
.s-toc .row.cur .num, .s-toc .row.cur .title { color: var(--pink); }
/* ============== 3. BY THE NUMBERS ======================== */
.s-stats .body { position: absolute; inset: 140px 60px 140px 60px; display: grid; grid-template-columns: 1fr 1.05fr; gap: 60px; }
.s-stats .left { display: flex; flex-direction: column; justify-content: space-between; padding-right: 20px; }
.s-stats .left .kicker { font-family: "JetBrains Mono", monospace; font-size: 24px; letter-spacing: .18em; text-transform: uppercase; color: var(--pink); }
.s-stats .left h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 132px; line-height: 1.06; color: var(--paper); padding-bottom: .1em; }
.s-stats .left h2 em { color: var(--pink); font-style: italic; }
.s-stats .left p { font-family: "Inter", sans-serif; font-size: 24px; line-height: 1.55; color: var(--paper); opacity: .75; max-width: 36ch; font-weight: 300; }
.s-stats .right { display: flex; flex-direction: column; gap: 18px; padding-top: 0; }
.s-stats .stat { display: grid; grid-template-columns: 240px 1fr; align-items: center; gap: 28px; padding-bottom: 16px; border-bottom: 1px solid var(--hair); }
.s-stats .stat:last-child { border-bottom: 0; padding-bottom: 0; }
.s-stats .stat .figure { font-family: "Instrument Serif", serif; font-style: italic; font-size: 116px; line-height: .9; color: var(--pink); display: flex; align-items: baseline; }
.s-stats .stat .figure sup { font-size: 36px; color: var(--paper); vertical-align: top; margin-left: 4px; line-height: 1; align-self: flex-start; padding-top: 18px; }
.s-stats .stat .meta .lab { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--paper); }
.s-stats .stat .meta .desc { font-family: "Inter", sans-serif; font-size: 24px; color: var(--mute); margin-top: 8px; line-height: 1.45; font-weight: 300; }
/* ============== 4. MOVEMENTS · SECTION DIVIDER =========== */
.s-section .body { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.s-section .body { justify-content: flex-start; padding-left: 200px; }
.s-section .num {
font-family: "Instrument Serif", serif; font-style: italic;
color: var(--pink);
font-size: 600px; line-height: .82; letter-spacing: -.02em;
text-shadow: 0 0 120px rgba(237,61,140,.22);
}
.s-section .label-l { position: absolute; left: 80px; top: auto; bottom: 140px; transform: rotate(-90deg); transform-origin: left bottom; font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .42em; text-transform: uppercase; color: var(--mute); white-space: nowrap; }
.s-section .right { position: absolute; right: 100px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 18px; max-width: 380px; }
.s-section .right .kicker { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--pink); }
.s-section .right h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 88px; line-height: 1.06; color: var(--paper); padding-bottom: .1em; }
.s-section .right p { font-family: "Inter", sans-serif; font-size: 24px; line-height: 1.55; color: var(--mute); font-weight: 300; }
/* ============== 5. THE CURVE · CHART ===================== */
.s-chart .head { position: absolute; left: 60px; right: 60px; top: 140px; display: flex; align-items: end; justify-content: space-between; gap: 80px; }
.s-chart .head h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 90px; line-height: 1.06; color: var(--paper); max-width: 18ch; padding-bottom: .1em; }
.s-chart .head h2 em { color: var(--pink); font-style: italic; }
.s-chart .head .legend { display: flex; flex-direction: column; gap: 14px; align-items: flex-end; }
.s-chart .head .legend .li { display: flex; align-items: center; gap: 14px; font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .12em; text-transform: uppercase; color: var(--paper); }
.s-chart .head .legend .li i { width: 36px; height: 2px; background: var(--pink); }
.s-chart .head .legend .li.b i { background: var(--paper); opacity: .5; }
.s-chart .plotwrap { position: absolute; left: 160px; right: 460px; top: 480px; bottom: 200px; }
.s-chart .plotwrap .yax { position: absolute; left: -100px; top: 0; bottom: 30px; display: flex; flex-direction: column; justify-content: space-between; font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .1em; color: var(--mute); align-items: flex-end; }
.s-chart .plotwrap .plot { position: absolute; inset: 0 0 30px 0; border-left: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.s-chart .plotwrap .plot .gline { position: absolute; left: 0; right: 0; border-top: 1px dashed rgba(237,61,140,.18); }
.s-chart .plotwrap .plot svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.s-chart .plotwrap .xax { position: absolute; left: 0; right: 0; bottom: 0; display: flex; justify-content: space-between; font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .1em; color: var(--mute); }
.s-chart .callout {
position: absolute; right: 60px; top: 480px; bottom: 200px;
display: flex; flex-direction: column; gap: 8px; width: 360px; align-items: flex-end; text-align: right; justify-content: flex-start;
border-left: 1px solid var(--pink); padding-left: 24px; padding-top: 4px;
}
.s-chart .callout .num { font-family: "Instrument Serif", serif; font-style: italic; font-size: 120px; line-height: .9; color: var(--pink); }
.s-chart .callout .lab { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--paper); }
.s-chart .callout .desc { font-family: "Inter", sans-serif; font-size: 22px; color: var(--mute); line-height: 1.4; font-weight: 300; }
/* ============== 6. PROCESS · DIAGRAM ===================== */
.s-process .head { position: absolute; left: 60px; right: 60px; top: 140px; display: flex; align-items: end; justify-content: space-between; gap: 60px; }
.s-process .head h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 156px; line-height: 1.04; color: var(--paper); max-width: 12ch; padding-bottom: .1em; }
.s-process .head h2 em { color: var(--pink); font-style: italic; }
.s-process .head .lead { font-family: "Inter", sans-serif; font-size: 26px; color: var(--mute); max-width: 40ch; line-height: 1.55; font-weight: 300; padding-bottom: 18px; }
.s-process .row { position: absolute; left: 60px; right: 60px; top: 540px; display: grid; grid-template-columns: repeat(5, 1fr); gap: 24px; }
.s-process .step { position: relative; display: flex; flex-direction: column; gap: 18px; padding: 26px 0; border-top: 1px solid var(--pink); }
.s-process .step .n { font-family: "Instrument Serif", serif; font-style: italic; font-size: 96px; color: var(--pink); line-height: .8; }
.s-process .step h3 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 38px; color: var(--paper); line-height: 1.05; }
.s-process .step p { font-family: "Inter", sans-serif; font-size: 22px; color: var(--mute); line-height: 1.5; font-weight: 300; }
.s-process .step .arr {
position: absolute; right: -16px; top: 60px; width: 24px; height: 24px;
color: var(--pink); pointer-events: none;
}
.s-process .step:last-child .arr { display: none; }
.s-process .timeline { position: absolute; left: 60px; right: 60px; bottom: 140px; display: flex; justify-content: space-between; font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .12em; text-transform: uppercase; color: var(--mute); padding-top: 16px; border-top: 1px solid var(--hair); }
.s-process .timeline span em { color: var(--pink); font-style: normal; }
/* ============== 7. THE FIELD · COMPARISON ================ */
.s-matrix .head { position: absolute; left: 60px; right: 60px; top: 140px; display: flex; align-items: end; justify-content: space-between; gap: 60px; }
.s-matrix .head h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 132px; line-height: 1.04; color: var(--paper); max-width: 14ch; padding-bottom: .1em; }
.s-matrix .head h2 em { color: var(--pink); font-style: italic; }
.s-matrix .head .source { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--mute); padding-bottom: 18px; max-width: 30ch; line-height: 1.5; text-align: right; }
.s-matrix .table { position: absolute; left: 60px; right: 60px; top: 460px; bottom: 140px; display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; grid-auto-rows: 1fr; }
.s-matrix .cell { padding: 16px 24px; border-bottom: 1px solid var(--line); display: flex; align-items: center; font-family: "Inter", sans-serif; font-size: 22px; line-height: 1.4; color: var(--paper); font-weight: 300; }
.s-matrix .cell.colhead { background: transparent; font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--pink); border-bottom-color: var(--pink); }
.s-matrix .cell.label { font-family: "Instrument Serif", serif; font-style: italic; font-size: 32px; color: var(--paper); }
.s-matrix .cell.us { background: rgba(237,61,140,.08); color: var(--paper); }
.s-matrix .pill { display: inline-block; padding: 6px 14px; font-family: "JetBrains Mono", monospace; font-size: 16px; letter-spacing: .08em; text-transform: uppercase; border: 1px solid var(--pink); white-space: nowrap; line-height: 1.2; color: var(--pink); }
.s-matrix .pill.dim { border-color: var(--hair); color: var(--mute); }
.s-matrix .pill.solid { background: var(--pink); color: #060507; border-color: var(--pink); font-weight: 500; }
/* ============== 8. VOICES · QUOTE ======================== */
.s-quote .body { position: absolute; inset: 140px 60px 140px 60px; display: grid; grid-template-columns: 320px 1fr; gap: 80px; align-items: center; }
.s-quote .left { display: flex; flex-direction: column; gap: 28px; }
.s-quote .left .qmark { font-family: "Instrument Serif", serif; font-style: italic; font-size: 320px; color: var(--pink); line-height: .65; }
.s-quote .left .lab { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--mute); }
.s-quote .right blockquote { font-family: "Instrument Serif", serif; font-style: italic; font-size: 92px; line-height: 1.05; color: var(--paper); letter-spacing: -.005em; }
.s-quote .right blockquote em { color: var(--pink); font-style: italic; }
.s-quote .attr { display: flex; align-items: baseline; gap: 24px; margin-top: 60px; padding-top: 28px; border-top: 1px solid var(--pink); }
.s-quote .attr .who { font-family: "Instrument Serif", serif; font-style: italic; font-size: 48px; color: var(--paper); }
.s-quote .attr .role { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--pink); }
/* ============== 9. ENCORE · CTA ========================== */
.s-cta .body { position: absolute; inset: 140px 60px 140px 60px; display: flex; flex-direction: column; justify-content: space-between; }
.s-cta .top { display: flex; flex-direction: column; gap: 12px; }
.s-cta .top .pre { font-family: "JetBrains Mono", monospace; font-size: 26px; letter-spacing: .24em; text-transform: uppercase; color: var(--pink); }
.s-cta .top h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 140px; line-height: 1.04; color: var(--paper); letter-spacing: -.015em; padding-bottom: .1em; }
.s-cta .top h2 em { color: var(--pink); font-style: italic; }
.s-cta .bottom { display: grid; grid-template-columns: repeat(3, 1fr) 280px; gap: 48px; align-items: end; }
.s-cta .step { display: flex; flex-direction: column; gap: 16px; padding-top: 22px; border-top: 1px solid var(--pink); }
.s-cta .step .n { font-family: "Instrument Serif", serif; font-style: italic; font-size: 64px; color: var(--pink); line-height: 1; }
.s-cta .step h3 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 44px; color: var(--paper); line-height: 1.05; }
.s-cta .step p { font-family: "Inter", sans-serif; font-size: 22px; color: var(--mute); line-height: 1.5; font-weight: 300; }
.s-cta .qr { display: flex; flex-direction: column; gap: 14px; align-items: flex-end; }
.s-cta .qr .box { width: 180px; height: 180px; background: var(--paper); padding: 12px; }
.s-cta .qr .box svg { width: 100%; height: 100%; }
.s-cta .qr .lab { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--paper); }
.s-cta .qr .url { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .04em; color: var(--pink); }
/* ============== 10. THE SYSTEM · DESIGN SYSTEM ============ */
.s-system .body { position: absolute; inset: 140px 60px 140px 60px; display: grid; grid-template-columns: 1fr 1fr; gap: 60px; }
.s-system .head { position: absolute; left: 60px; right: 60px; top: 140px; }
.s-system .head h2 { font-family: "Instrument Serif", serif; font-style: italic; font-size: 132px; color: var(--paper); }
.s-system .head h2 em { color: var(--pink); font-style: italic; }
.s-system .body { top: 320px; }
.s-system .panel { display: flex; flex-direction: column; gap: 22px; padding-top: 22px; border-top: 1px solid var(--pink); }
.s-system .panel h3 { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .14em; text-transform: uppercase; color: var(--pink); }
.s-system .palette { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.s-system .swatch { display: flex; flex-direction: column; gap: 8px; }
.s-system .swatch .chip { aspect-ratio: 1; border: 1px solid var(--hair); }
.s-system .swatch .name { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .08em; color: var(--paper); }
.s-system .swatch .hex { font-family: "JetBrains Mono", monospace; font-size: 22px; color: var(--mute); }
.s-system .typespec { display: flex; flex-direction: column; gap: 14px; }
.s-system .typerow { display: grid; grid-template-columns: 240px 1fr; gap: 24px; align-items: baseline; padding-bottom: 12px; border-bottom: 1px dashed var(--hair); }
.s-system .typerow .meta { font-family: "JetBrains Mono", monospace; font-size: 22px; letter-spacing: .08em; color: var(--mute); text-transform: uppercase; }
.s-system .typerow .sample { color: var(--paper); }
.s-system .typerow .sample.script { font-family: "Instrument Serif", serif; font-style: italic; color: var(--pink); }
.s-system .typerow .sample.disp { font-family: "Instrument Serif", serif; font-style: italic; }
.s-system .typerow .sample.body { font-family: "Inter", sans-serif; font-weight: 300; }
.s-system .typerow .sample.mono { font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: .12em; }
.s-system .rules { display: flex; flex-direction: column; gap: 12px; }
.s-system .rules .item { display: grid; grid-template-columns: 64px 1fr; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--hair); }
.s-system .rules .item .n { font-family: "Instrument Serif", serif; font-style: italic; font-size: 48px; color: var(--pink); line-height: .9; }
.s-system .rules .item p { font-family: "Inter", sans-serif; font-size: 22px; color: var(--paper); line-height: 1.45; font-weight: 300; }
.s-system .rules .item p strong { color: var(--pink); font-weight: 500; font-style: italic; font-family: "Instrument Serif", serif; font-size: 26px; padding-right: 6px; }
</style>
</head>
<body>
<deck-stage>
<!-- ============== 1. COVER · AFTER HOURS ===================== -->
<section class="slide s-cover" data-label="01 Cover" data-om-validate="false">
<div class="runner">
<span class="brand">Maison Nocturne</span>
<span>Vol. XIV · A/W 2026</span>
</div>
<div class="stage">
<div class="pre">A Field Report on Late-Night Couture</div>
<div class="title-wrap">
<div class="title">After<span class="l2">Hours.</span></div>
</div>
<div class="sub" style="margin:0"></div>
</div>
<div class="lower">
<div class="col">
<div class="lab">Edition</div>
<div class="val">No. 14</div>
</div>
<div class="col">
<div class="lab">Director</div>
<div class="val alt">L. Marchetti</div>
</div>
<div class="col">
<div class="lab">Locale</div>
<div class="val alt">Paris · 11<sup style="font-size:.5em">e</sup></div>
</div>
<div class="col">
<div class="lab">Date</div>
<div class="val">May 2026</div>
</div>
</div>
<div class="footer">
<span>Maison Nocturne · Confidential</span>
<span class="pageno"><em>01</em> / 09</span>
</div>
</section>
<!-- ============== 2. THE INDEX ============================== -->
<section class="slide s-toc" data-label="02 The Index" data-om-validate="false">
<div class="runner"><span class="brand">After Hours</span><span>The Index</span></div>
<div class="body">
<h1>The<br><span class="small">Index.</span></h1>
<div class="rows">
<div class="row">
<div class="num">01</div>
<div>
<div class="title">By the Numbers</div>
<div class="desc">Five figures that shape the season.</div>
</div>
<div class="meta">Stats · pp. 14</div>
</div>
<div class="row cur">
<div class="num">02</div>
<div>
<div class="title">Movements</div>
<div class="desc">A study in cuts, color, and silhouette.</div>
</div>
<div class="meta">Section · pp. 22</div>
</div>
<div class="row">
<div class="num">03</div>
<div>
<div class="title">The Curve</div>
<div class="desc">Twelve weeks of after-hours behavior.</div>
</div>
<div class="meta">Chart · pp. 36</div>
</div>
<div class="row">
<div class="num">04</div>
<div>
<div class="title">The Field</div>
<div class="desc">Where we sit among the houses we admire.</div>
</div>
<div class="meta">Matrix · pp. 48</div>
</div>
<div class="row">
<div class="num">05</div>
<div>
<div class="title">Voices &amp; Encore</div>
<div class="desc">Critics, clients, and what comes next.</div>
</div>
<div class="meta">pp. 6072</div>
</div>
</div>
</div>
<div class="footer"><span>Maison Nocturne</span><span class="pageno"><em>02</em> / 09</span></div>
</section>
<!-- ============== 3. BY THE NUMBERS ========================= -->
<section class="slide s-stats" data-label="03 By the Numbers" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 01</span><span>By the Numbers · A/W26</span></div>
<div class="body">
<div class="left">
<div class="kicker">By the Numbers</div>
<div>
<h2>A season<br>told in<br><em>five</em> figures.</h2>
</div>
<p>Read top to bottom. Every figure was reported by atelier directors during the eight-week previewing window and represents the house ledger only.</p>
</div>
<div class="right">
<div class="stat">
<div class="figure">42<sup>%</sup></div>
<div class="meta">
<div class="lab">Couture · Repeat Clients</div>
<div class="desc">Patrons who returned within ninety days for a second commission.</div>
</div>
</div>
<div class="stat">
<div class="figure">3.8<sup>×</sup></div>
<div class="meta">
<div class="lab">Atelier Throughput</div>
<div class="desc">Pieces released per machinist per week, measured against the prior Spring book.</div>
</div>
</div>
<div class="stat">
<div class="figure">€1.4<sup>M</sup></div>
<div class="meta">
<div class="lab">Average Ticket · Vault</div>
<div class="desc">Mean spend per private appointment in the Vault programme this quarter.</div>
</div>
</div>
<div class="stat">
<div class="figure">86<sup>%</sup></div>
<div class="meta">
<div class="lab">Reservation Rate</div>
<div class="desc">Show seats filled before the public window opened.</div>
</div>
</div>
<div class="stat">
<div class="figure">07</div>
<div class="meta">
<div class="lab">New Cities, A/W</div>
<div class="desc">Markets opened with a flagship boutique since the prior season.</div>
</div>
</div>
</div>
</div>
<div class="footer"><span>Source · Atelier Ledger Q1</span><span class="pageno"><em>03</em> / 09</span></div>
</section>
<!-- ============== 4. MOVEMENTS · SECTION DIVIDER ============= -->
<section class="slide s-section" data-label="04 Movements" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 02</span><span>Movements</span></div>
<div class="label-l">Maison Nocturne · Vol. XIV</div>
<div class="body">
<div class="num">02</div>
</div>
<div class="right">
<div class="kicker">Movements</div>
<h2>A study<br>in cuts<br>&amp; color.</h2>
<p>Three silhouettes carry the season — the column, the cape, and the cinch. Each is annotated in the chapters that follow.</p>
</div>
<div class="footer"><span>Chapter 02 of 05</span><span class="pageno"><em>04</em> / 09</span></div>
</section>
<!-- ============== 5. THE CURVE · CHART ====================== -->
<section class="slide s-chart" data-label="05 The Curve" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 03</span><span>The Curve</span></div>
<div class="head">
<h2>Twelve weeks of <em>after-hours</em><br>behavior.</h2>
<div class="legend">
<div class="li"><i></i>House · A/W26</div>
<div class="li b"><i></i>Sector benchmark</div>
</div>
</div>
<div class="callout">
<div class="num">+38<span style="font-size:.5em;color:var(--paper)">%</span></div>
<div class="lab">Week 09 inflection</div>
<div class="desc">After the editorial dropped, walk-ins to the rue Saint-Honoré flagship doubled within seventy-two hours.</div>
</div>
<div class="plotwrap">
<div class="yax">
<span>200</span><span>150</span><span>100</span><span>50</span><span>0</span>
</div>
<div class="plot">
<div class="gline" style="top:0%"></div>
<div class="gline" style="top:25%"></div>
<div class="gline" style="top:50%"></div>
<div class="gline" style="top:75%"></div>
<svg viewBox="0 0 1200 400" preserveAspectRatio="none">
<!-- benchmark (dim white) -->
<polyline fill="none" stroke="rgba(245,237,241,.45)" stroke-width="2" stroke-dasharray="6 6"
points="0,310 100,300 200,295 300,290 400,285 500,280 600,272 700,265 800,260 900,255 1000,250 1100,245 1200,242"/>
<!-- house line (pink) -->
<polyline fill="none" stroke="#ED3D8C" stroke-width="3"
points="0,330 100,318 200,300 300,288 400,272 500,250 600,232 700,210 800,140 900,120 1000,108 1100,98 1200,92"/>
<!-- inflection marker -->
<circle cx="800" cy="140" r="9" fill="#ED3D8C"/>
<circle cx="800" cy="140" r="18" fill="none" stroke="#ED3D8C" stroke-width="1.5" opacity=".5"/>
<line x1="800" y1="140" x2="800" y2="400" stroke="#ED3D8C" stroke-width="1" stroke-dasharray="4 6" opacity=".5"/>
</svg>
</div>
<div class="xax">
<span>W01</span><span>W02</span><span>W03</span><span>W04</span><span>W05</span><span>W06</span><span>W07</span><span>W08</span><span style="color:var(--pink)">W09</span><span>W10</span><span>W11</span><span>W12</span>
</div>
</div>
<div class="footer"><span>Source · House register · Index FY25=100</span><span class="pageno"><em>05</em> / 09</span></div>
</section>
<!-- ============== 6. PROCESS · DIAGRAM ====================== -->
<section class="slide s-process" data-label="06 Process" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 04</span><span>The Method</span></div>
<div class="head">
<h2>The<br><em>method.</em></h2>
<div class="lead">From sketchbook to runway in five movements. The atelier's tempo is dictated by the cloth, never the calendar.</div>
</div>
<div class="row">
<div class="step">
<div class="n">01</div>
<h3>Brief</h3>
<p>The house director and head couturier convene with three muses to set the season's mood.</p>
<svg class="arr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M14 5l7 7-7 7"/></svg>
</div>
<div class="step">
<div class="n">02</div>
<h3>Pattern</h3>
<p>Toiles cut in calico. Each silhouette is fitted three times before approval is granted on the floor.</p>
<svg class="arr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M14 5l7 7-7 7"/></svg>
</div>
<div class="step">
<div class="n">03</div>
<h3>Atelier</h3>
<p>Cloth is cut on the bias. Hand-stitched seams. No piece leaves the atelier without two signatures.</p>
<svg class="arr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M14 5l7 7-7 7"/></svg>
</div>
<div class="step">
<div class="n">04</div>
<h3>Fitting</h3>
<p>Private appointments held by candlelight in the Vault. Clients touch the cloth before the look is final.</p>
<svg class="arr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M14 5l7 7-7 7"/></svg>
</div>
<div class="step">
<div class="n">05</div>
<h3>Runway</h3>
<p>Twelve looks shown. The collection is sold by appointment for ninety days before the public window opens.</p>
</div>
</div>
<div class="timeline">
<span>Wk 0102 <em>Brief</em></span>
<span>Wk 0306 <em>Pattern</em></span>
<span>Wk 0710 <em>Atelier</em></span>
<span>Wk 1112 <em>Fitting</em></span>
<span>Wk 13 <em>Runway</em></span>
</div>
<div class="footer"><span>Atelier Method · House Standard</span><span class="pageno"><em>06</em> / 09</span></div>
</section>
<!-- ============== 7. THE FIELD · COMPARISON MATRIX ========== -->
<section class="slide s-matrix" data-label="07 The Field" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 05</span><span>The Field</span></div>
<div class="head">
<h2>The<br><em>field</em>, in five rows.</h2>
<div class="source">Sourced · house registers, public filings, three trade press indices · A/W 2026</div>
</div>
<div class="table">
<div class="cell colhead">Dimension</div>
<div class="cell colhead" style="color:var(--pink)">Maison Nocturne</div>
<div class="cell colhead">House A</div>
<div class="cell colhead">House B</div>
<div class="cell label">Atelier model</div>
<div class="cell us"><span class="pill solid">In-house · Paris</span></div>
<div class="cell"><span class="pill">Hybrid · 2 cities</span></div>
<div class="cell"><span class="pill dim">Outsourced</span></div>
<div class="cell label">Lead time</div>
<div class="cell us">13 weeks, hand-stitched</div>
<div class="cell">9 weeks, partial machine</div>
<div class="cell">6 weeks, full machine</div>
<div class="cell label">Vault programme</div>
<div class="cell us"><span class="pill solid">Yes · invitation</span></div>
<div class="cell"><span class="pill dim">No</span></div>
<div class="cell"><span class="pill">By appointment</span></div>
<div class="cell label">Repeat client share</div>
<div class="cell us"><strong style="color:var(--pink);font-family:'Instrument Serif';font-style:italic;font-size:32px">42%</strong></div>
<div class="cell">28%</div>
<div class="cell">19%</div>
<div class="cell label" style="border-bottom:0">Public window</div>
<div class="cell us" style="border-bottom:0">90 days post-show</div>
<div class="cell" style="border-bottom:0">30 days post-show</div>
<div class="cell" style="border-bottom:0">Same day</div>
</div>
<div class="footer"><span>Comparison · A/W 2026 disclosed</span><span class="pageno"><em>07</em> / 09</span></div>
</section>
<!-- ============== 8. VOICES · QUOTE ========================= -->
<section class="slide s-quote" data-label="08 Voices" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 06</span><span>Voices</span></div>
<div class="body">
<div class="left">
<div class="qmark">"</div>
<div class="lab">Voices · Issue 14</div>
</div>
<div class="right">
<blockquote>
The house dresses you for an <em>evening</em> that hasn't begun. You leave the fitting and somewhere a room is already <em>waiting</em>.
</blockquote>
<div class="attr">
<div class="who">— Camille Aubry</div>
<div class="role">Editor-in-chief · Le Soir Parisien</div>
</div>
</div>
</div>
<div class="footer"><span>Voices · Le Soir Parisien</span><span class="pageno"><em>08</em> / 09</span></div>
</section>
<!-- ============== 9. ENCORE · CTA =========================== -->
<section class="slide s-cta" data-label="09 Encore" data-om-validate="false">
<div class="runner"><span class="brand">Chapter 07</span><span>Encore</span></div>
<div class="body">
<div class="top">
<div class="pre">An invitation</div>
<h2><em>Encore.</em><br>The list opens<br>this Friday.</h2>
</div>
<div class="bottom">
<div class="step">
<div class="n">01</div>
<h3>Reserve</h3>
<p>Hold a Vault appointment for the week of 24 May. Couture only.</p>
</div>
<div class="step">
<div class="n">02</div>
<h3>Preview</h3>
<p>Three looks shown by candlelight in the rue Saint-Honoré room.</p>
</div>
<div class="step">
<div class="n">03</div>
<h3>Commission</h3>
<p>One piece commissioned to your measure, delivered before September.</p>
</div>
<div class="qr">
<div class="box">
<svg viewBox="0 0 25 25" shape-rendering="crispEdges">
<rect width="25" height="25" fill="#F5EDF1"/>
<g fill="#060507">
<rect x="0" y="0" width="7" height="7"/><rect x="2" y="2" width="3" height="3" fill="#F5EDF1"/>
<rect x="18" y="0" width="7" height="7"/><rect x="20" y="2" width="3" height="3" fill="#F5EDF1"/>
<rect x="0" y="18" width="7" height="7"/><rect x="2" y="20" width="3" height="3" fill="#F5EDF1"/>
<rect x="9" y="0" width="1" height="1"/><rect x="11" y="0" width="2" height="1"/><rect x="14" y="1" width="1" height="2"/>
<rect x="8" y="3" width="2" height="2"/><rect x="12" y="3" width="3" height="1"/><rect x="9" y="5" width="1" height="2"/>
<rect x="11" y="5" width="2" height="2"/><rect x="14" y="6" width="2" height="1"/>
<rect x="0" y="9" width="2" height="1"/><rect x="3" y="9" width="2" height="2"/><rect x="6" y="9" width="3" height="1"/>
<rect x="10" y="9" width="2" height="3"/><rect x="14" y="9" width="2" height="2"/><rect x="17" y="9" width="2" height="1"/>
<rect x="20" y="9" width="3" height="2"/><rect x="0" y="12" width="3" height="1"/><rect x="5" y="12" width="2" height="2"/>
<rect x="9" y="12" width="2" height="1"/><rect x="13" y="12" width="3" height="2"/><rect x="18" y="12" width="2" height="3"/>
<rect x="22" y="12" width="2" height="1"/><rect x="0" y="15" width="2" height="2"/><rect x="4" y="15" width="3" height="1"/>
<rect x="9" y="15" width="3" height="2"/><rect x="14" y="15" width="2" height="2"/><rect x="20" y="15" width="2" height="2"/>
<rect x="9" y="18" width="2" height="1"/><rect x="13" y="18" width="3" height="3"/><rect x="18" y="18" width="2" height="3"/>
<rect x="22" y="18" width="2" height="2"/><rect x="9" y="21" width="3" height="2"/><rect x="17" y="22" width="3" height="2"/>
<rect x="22" y="22" width="3" height="3"/>
</g>
</svg>
</div>
<div class="lab">Scan to reserve</div>
<div class="url">nocturne.house/aw26</div>
</div>
</div>
</div>
<div class="footer"><span>RSVP closes 22 May · Strict</span><span class="pageno"><em>09</em> / 09</span></div>
</section>
</deck-stage>
<script type="application/json" id="speaker-notes">
[
"Cover slide. After Hours, an editorial-style deck for a fashion house presentation. Italic script display type, hot pink on deep black with a subtle radial spotlight and film grain.",
"The Index. Five chapters laid out as an editorial table of contents.",
"By the Numbers. Five hero figures, italic script numerals on pink, supporting copy in Inter Light.",
"Movements. The big 02 acts as the section divider, mirrored by a small kicker and copy on the right.",
"The Curve. A line chart with one pink hero series and a dashed white benchmark, plus a callout block for the inflection.",
"Process. Five steps with italic numerals, divider rules, and a horizontal timeline.",
"The Field. Comparison matrix using the pink-tinted column to spotlight our house.",
"Voices. Pull quote in italic script, with one or two phrases pinked for emphasis.",
"Encore. The CTA — three steps and a QR code in paper-on-ink."
]
</script>
</body>
</html>

View File

@@ -0,0 +1,46 @@
{
"slug": "pink-script",
"name": "Pink Script \u2014 After Hours",
"tagline": "Black canvas, hot pink accent, pearl-cream paper, Instrument Serif headlines: late-night editorial luxury.",
"mood": [
"nocturnal",
"moody",
"intentional",
"luxe",
"expressive"
],
"occasion": [
"fashion brand deck",
"creator personal brand",
"after-hours product (nightlife / dating / spirits)",
"luxury launch",
"editorial feature"
],
"tone": [
"literary",
"sultry",
"considered",
"magazine"
],
"formality": "medium-high",
"density": "low",
"palette": {
"ink": "#060507",
"paper": "#F5EDF1",
"pink": "#ED3D8C",
"pink_2": "#FF66A8",
"pink_deep": "#B81D67",
"description": "near-black canvas with one saturated hot pink accent and a pearl-cream paper for content; the whole system runs on a single accent + restraint"
},
"typography": {
"display": "Instrument Serif",
"body": "Inter",
"mono": "JetBrains Mono",
"style": "sharp transitional serif headlines + clean sans body + technical mono labels"
},
"scheme": "dark",
"best_for": "Anything that should feel nocturnal, intentional, and a little luxe: fashion brand decks, creator personal brands, after-hours / nightlife / spirits launches, luxury product reveals, editorial features. Also a striking unexpected pick for a tech keynote, research synthesis, or business pitch that wants to land with magnetic confidence.",
"avoid_for": "Daytime corporate-professional and traditional B2B contexts where the dark canvas with hot-pink accent reads as too styled or too expressive.",
"slide_count": 9,
"navigation": "deck-stage runtime (arrow keys, space, PgUp/PgDn, Home/End)"
}