mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
Section 03 + easter egg fixes — slop panel theme-adapts · easter actually fires
- Section 03 "Without Hallmark" panel no longer hardcodes #fff bg; uses var(--color-paper) so Manifesto / Midnight / Terminal / Brutal don't show a jarring white box mid-page (the AI-slop tells are still carried by the gradient title, Inter, centred layout, pill CTA) - Removed four dead per-theme .vs__col:first-child colour overrides (Manifesto / Sport / Brutal / Terminal) — they were painting the old CSS-mockup column, now clashing with the panel-based demo - Easter egg fix: previous version showed for one frame and was dismissed by the next autorepeat keydown. Now swallows every key while open, auto-dismisses after 4.2s with a fade-out, threshold lowered to 12 so it triggers reliably - Easter visual: solid #fff, system mono, #111 text, dark cursor — decoupled from theme so it reads as "the page steps in" not "another theme" - Random punchline picked from 6 lines ("chill, designer.", "sixteen is plenty.", etc.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
ab39afe87c
commit
2379a5ca78
+69
-78
@@ -601,18 +601,22 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* — "Without Hallmark" — the slop stack ————————————————— */
|
||||
/* — "Without Hallmark" — the slop stack —————————————————
|
||||
The bg uses theme paper so the panel integrates on dark themes
|
||||
(otherwise a pure-white card looks like a glitch on Manifesto /
|
||||
Midnight / Terminal). The AI-slop tells are still carried by the
|
||||
centred layout, Inter font, gradient title, and gradient pill. */
|
||||
.vs__panel[data-variant="slop"] {
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
background: #fff;
|
||||
border-color: #e6e6ec;
|
||||
background: var(--color-paper);
|
||||
border-color: var(--color-rule);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.vs__panel[data-variant="slop"] .vs__panel-eyebrow {
|
||||
color: #6b6b76;
|
||||
color: var(--color-muted);
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
@@ -623,7 +627,7 @@
|
||||
font-family: inherit;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
background: linear-gradient(95deg, #6b4de0 0%, #c14de0 50%, #e04db4 100%);
|
||||
background: linear-gradient(95deg, #8b5dff 0%, #c14de0 50%, #e04db4 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
@@ -631,7 +635,7 @@
|
||||
}
|
||||
|
||||
.vs__panel[data-variant="slop"] .vs__panel-sub {
|
||||
color: #6b6b76;
|
||||
color: var(--color-muted);
|
||||
font-family: inherit;
|
||||
max-width: 28ch;
|
||||
}
|
||||
@@ -982,8 +986,10 @@
|
||||
|
||||
/* — Easter egg overlay ——————————————————————————————————
|
||||
Hidden by default. Revealed when the user spams T faster than the
|
||||
site can keep up — see js/main.js. Uses theme tokens so the panel
|
||||
adapts to whichever theme is active when it fires. */
|
||||
site can keep up — see js/main.js. Pure white full-screen, system
|
||||
mono font, dark text — deliberately decoupled from the active
|
||||
theme so it reads as the page itself stepping in. Auto-dismisses
|
||||
after ~4.2s; while it's open, all keystrokes are swallowed. */
|
||||
.easter {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -991,40 +997,35 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-xl);
|
||||
background: color-mix(in oklch, var(--color-paper) 92%, transparent);
|
||||
backdrop-filter: blur(10px) saturate(1.1);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(1.1);
|
||||
animation: easter-fade-in 280ms var(--ease-out) both;
|
||||
background: #fff;
|
||||
color: #111;
|
||||
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
animation: easter-fade-in 240ms ease-out both;
|
||||
}
|
||||
|
||||
.easter[hidden] { display: none; }
|
||||
|
||||
.easter[data-state="closing"] {
|
||||
animation: easter-fade-out 360ms ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes easter-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.easter__panel {
|
||||
display: grid;
|
||||
gap: var(--space-xl);
|
||||
max-width: 52rem;
|
||||
width: 100%;
|
||||
padding: var(--space-2xl);
|
||||
background: var(--color-paper);
|
||||
border: var(--rule-hair) solid var(--color-rule);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.7;
|
||||
color: var(--color-ink);
|
||||
@keyframes easter-fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
[data-theme="brutal"] .easter__panel,
|
||||
[data-theme="manifesto"] .easter__panel { border-width: 2px; }
|
||||
|
||||
[data-theme="terminal"] .easter__panel,
|
||||
[data-theme="midnight"] .easter__panel,
|
||||
[data-theme="manifesto"] .easter__panel {
|
||||
background: var(--color-paper-2);
|
||||
.easter__panel {
|
||||
display: grid;
|
||||
gap: 1.4rem;
|
||||
max-width: 38rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.easter__lines {
|
||||
@@ -1032,75 +1033,73 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.easter__line {
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
animation: easter-line 360ms var(--ease-out) forwards;
|
||||
animation: easter-line 360ms ease-out forwards;
|
||||
}
|
||||
|
||||
.easter__line:nth-child(1) { animation-delay: 80ms; }
|
||||
.easter__line:nth-child(2) { animation-delay: 280ms; }
|
||||
.easter__line:nth-child(3) { animation-delay: 460ms; }
|
||||
.easter__line:nth-child(4) { animation-delay: 620ms; }
|
||||
.easter__line:nth-child(5) { animation-delay: 880ms; }
|
||||
.easter__line:nth-child(1) { animation-delay: 180ms; }
|
||||
.easter__line:nth-child(2) { animation-delay: 380ms; }
|
||||
.easter__line:nth-child(3) { animation-delay: 560ms; }
|
||||
.easter__line:nth-child(4) { animation-delay: 720ms; }
|
||||
.easter__line:nth-child(5) { animation-delay: 1000ms; }
|
||||
|
||||
@keyframes easter-line {
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.easter__line--cmd {
|
||||
font-size: var(--text-md);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-ink);
|
||||
margin-block-end: 0.4rem;
|
||||
color: #111;
|
||||
margin-block-end: 0.6rem;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.easter__prompt,
|
||||
.easter__arrow {
|
||||
color: var(--color-accent);
|
||||
color: #b0b0b0;
|
||||
user-select: none;
|
||||
margin-inline-end: 0.4rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.easter__num {
|
||||
color: var(--color-ink);
|
||||
font-weight: 500;
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
font-feature-settings: "tnum";
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.easter__line--punchline {
|
||||
margin-block-start: 0.6rem;
|
||||
font-family: var(--font-display);
|
||||
font-style: var(--display-style, italic);
|
||||
font-weight: var(--display-weight, 400);
|
||||
font-size: clamp(1.5rem, 4vw, 2.25rem);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--color-ink);
|
||||
margin-block-start: 0.8rem;
|
||||
font-size: clamp(1.75rem, 4.5vw, 2.5rem);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 500;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.easter__line--punchline .easter__arrow {
|
||||
font-family: var(--font-mono);
|
||||
font-style: normal;
|
||||
font-size: var(--text-sm);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
vertical-align: 0.25em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.easter__cursor {
|
||||
display: inline-block;
|
||||
width: 0.5em;
|
||||
height: 1em;
|
||||
background: var(--color-ink);
|
||||
margin-inline-start: 0.2em;
|
||||
vertical-align: -0.12em;
|
||||
animation: easter-blink 1.05s steps(2) infinite;
|
||||
animation-delay: 1.3s;
|
||||
width: 0.42em;
|
||||
height: 0.95em;
|
||||
background: #111;
|
||||
margin-inline-start: 0.18em;
|
||||
vertical-align: -0.08em;
|
||||
opacity: 0;
|
||||
animation: easter-blink 1.05s steps(2) infinite;
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
|
||||
@keyframes easter-blink {
|
||||
@@ -1110,29 +1109,21 @@
|
||||
|
||||
.easter__hint {
|
||||
margin: 0;
|
||||
padding-block-start: var(--space-md);
|
||||
border-block-start: var(--rule-hair) solid var(--color-rule);
|
||||
font-family: var(--font-label);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-label);
|
||||
padding-block-start: 1.2rem;
|
||||
border-block-start: 1px solid #e5e5e5;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
color: #888;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.easter__hint-key {
|
||||
color: var(--color-ink);
|
||||
border: var(--rule-hair) solid var(--color-rule);
|
||||
padding: 0.18rem 0.55rem;
|
||||
}
|
||||
|
||||
.easter__hint-sep { color: var(--color-rule-2); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.easter,
|
||||
.easter[data-state="closing"],
|
||||
.easter__line { animation: none; }
|
||||
.easter__line { opacity: 1; transform: none; }
|
||||
.easter__cursor { animation: none; opacity: 1; }
|
||||
|
||||
@@ -569,7 +569,6 @@
|
||||
animation: blink 1.05s steps(2) infinite;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
[data-theme="terminal"] .vs__col:first-child { background: var(--color-paper-3); }
|
||||
[data-theme="terminal"] .step__num::before { content: "[ "; color: var(--color-accent); }
|
||||
[data-theme="terminal"] .step__num::after { content: " ]"; color: var(--color-accent); }
|
||||
|
||||
@@ -611,15 +610,6 @@
|
||||
max-width: 18ch;
|
||||
font-weight: 800;
|
||||
}
|
||||
[data-theme="manifesto"] .vs { border: 3px solid var(--color-ink); }
|
||||
[data-theme="manifesto"] .vs__col:first-child {
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
}
|
||||
[data-theme="manifesto"] .vs__col:first-child .vs__label,
|
||||
[data-theme="manifesto"] .vs__col:first-child .vs__caption {
|
||||
color: var(--color-paper);
|
||||
}
|
||||
[data-theme="manifesto"] .step { border-top: 3px solid var(--color-ink); }
|
||||
|
||||
/* ── SALON — centered narrow, fleuron divider ───────────────── */
|
||||
@@ -771,14 +761,6 @@ html[data-theme="almanac"] body {
|
||||
}
|
||||
@media (max-width: 56rem) { [data-theme="sport"] .foundations { grid-template-columns: 1fr; } }
|
||||
[data-theme="sport"] .spec { border-top: var(--rule-hair) solid var(--color-rule-2); }
|
||||
[data-theme="sport"] .vs__col:first-child {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
}
|
||||
[data-theme="sport"] .vs__col:first-child .vs__label,
|
||||
[data-theme="sport"] .vs__col:first-child .vs__caption {
|
||||
color: var(--color-paper);
|
||||
}
|
||||
[data-theme="sport"] .step__num {
|
||||
font-style: italic;
|
||||
color: var(--color-accent);
|
||||
@@ -796,14 +778,6 @@ html[data-theme="almanac"] body {
|
||||
[data-theme="brutal"] .section__head {
|
||||
border-bottom: 3px solid var(--color-ink);
|
||||
}
|
||||
[data-theme="brutal"] .vs__col:first-child {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
}
|
||||
[data-theme="brutal"] .vs__col:first-child .vs__label,
|
||||
[data-theme="brutal"] .vs__col:first-child .vs__caption {
|
||||
color: var(--color-paper);
|
||||
}
|
||||
|
||||
/* ── GARDEN existing — augment ──────────────────────────────── */
|
||||
[data-theme="garden"] .section__head {
|
||||
|
||||
+3
-5
@@ -576,14 +576,12 @@
|
||||
</li>
|
||||
<li class="easter__line"><span class="easter__arrow">></span> there are <span class="easter__num">16</span> themes.</li>
|
||||
<li class="easter__line"><span class="easter__arrow">></span> there is no theme <span class="easter__num">17</span>.</li>
|
||||
<li class="easter__line easter__line--punchline">
|
||||
<span class="easter__arrow">></span> chill, designer.<span class="easter__cursor" aria-hidden="true"></span>
|
||||
<li class="easter__line easter__line--punchline" data-easter-punchline>
|
||||
<span class="easter__arrow">></span> <span data-easter-line>chill, designer.</span><span class="easter__cursor" aria-hidden="true"></span>
|
||||
</li>
|
||||
</ol>
|
||||
<p class="easter__hint">
|
||||
<span class="easter__hint-key">tap any key</span>
|
||||
<span class="easter__hint-sep" aria-hidden="true">·</span>
|
||||
<span>to return to the page</span>
|
||||
<span>returning to the page in a moment</span>
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
+52
-25
@@ -568,27 +568,41 @@ if (shuffleBtn) {
|
||||
}
|
||||
|
||||
/* — Easter egg — "chill, designer." ————————————————————————
|
||||
Spam T fast enough and the page intervenes. We track timestamps in a
|
||||
rolling 3.2-second window; if the user crosses the threshold (≈ a
|
||||
full catalog cycle inside the window), we show an overlay and reset
|
||||
the counter. A 30-second cooldown stops it from firing twice in a row. */
|
||||
Spam T fast enough and the page intervenes. We track timestamps in
|
||||
a rolling 3.2s window; if the user crosses the threshold (≈ a full
|
||||
catalog cycle inside the window), the overlay takes over. While
|
||||
it's visible, every keystroke is swallowed — no theme cycling, no
|
||||
shortcuts. After ~4s the overlay fades and the page returns. A 30s
|
||||
cooldown stops it from firing back-to-back. */
|
||||
const easterEl = document.querySelector("[data-easter-egg]");
|
||||
const EASTER_WINDOW_MS = 3200;
|
||||
const EASTER_THRESHOLD = 14; // ≈ 4.4 presses/sec
|
||||
const EASTER_THRESHOLD = 12; // ≈ 3.8 presses/sec
|
||||
const EASTER_VISIBLE_MS = 4200; // total time the overlay stays up
|
||||
const EASTER_FADE_MS = 360; // matches the fade-out animation
|
||||
const EASTER_COOLDOWN_MS = 30000;
|
||||
const EASTER_PUNCHLINES = [
|
||||
"chill, designer.",
|
||||
"you've seen them all.",
|
||||
"sixteen is plenty.",
|
||||
"pick. build. ship.",
|
||||
"easy on the keyboard.",
|
||||
"one theme will do.",
|
||||
];
|
||||
const tStamps = [];
|
||||
let easterLastFired = 0;
|
||||
let easterOpen = false;
|
||||
let easterDismissTimer = null;
|
||||
let easterFadeTimer = null;
|
||||
|
||||
function showEasterEgg() {
|
||||
if (!easterEl) return;
|
||||
if (!easterEl || easterOpen) return;
|
||||
const now = performance.now();
|
||||
const span = (now - tStamps[0]) / 1000;
|
||||
const cycles = (tStamps.length / Object.keys(THEMES).length).toFixed(1);
|
||||
|
||||
// Re-mount the lines container so the staggered fade-in animation
|
||||
// re-triggers each time the easter egg is shown. Populate the live
|
||||
// values after replacement so we don't write into a detached node.
|
||||
// Re-mount the lines so the staggered fade-in animation re-runs each
|
||||
// time. Populate live values + a random punchline after replacement
|
||||
// so we don't write into a detached node.
|
||||
const lines = easterEl.querySelector(".easter__lines");
|
||||
if (lines) {
|
||||
const fresh = lines.cloneNode(true);
|
||||
@@ -596,32 +610,42 @@ function showEasterEgg() {
|
||||
}
|
||||
const cyclesEl = easterEl.querySelector("[data-easter-cycles]");
|
||||
const secondsEl = easterEl.querySelector("[data-easter-seconds]");
|
||||
const lineEl = easterEl.querySelector("[data-easter-line]");
|
||||
if (cyclesEl) cyclesEl.textContent = cycles;
|
||||
if (secondsEl) secondsEl.textContent = span.toFixed(1);
|
||||
if (lineEl) lineEl.textContent = EASTER_PUNCHLINES[Math.floor(Math.random() * EASTER_PUNCHLINES.length)];
|
||||
|
||||
delete easterEl.dataset.state;
|
||||
easterEl.hidden = false;
|
||||
easterOpen = true;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
clearTimeout(easterDismissTimer);
|
||||
clearTimeout(easterFadeTimer);
|
||||
easterDismissTimer = setTimeout(hideEasterEgg, EASTER_VISIBLE_MS);
|
||||
}
|
||||
|
||||
function hideEasterEgg() {
|
||||
if (!easterEl || !easterOpen) return;
|
||||
easterEl.hidden = true;
|
||||
easterOpen = false;
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
if (easterEl) {
|
||||
easterEl.addEventListener("click", hideEasterEgg);
|
||||
clearTimeout(easterDismissTimer);
|
||||
clearTimeout(easterFadeTimer);
|
||||
easterEl.dataset.state = "closing";
|
||||
easterFadeTimer = setTimeout(() => {
|
||||
easterEl.hidden = true;
|
||||
delete easterEl.dataset.state;
|
||||
easterOpen = false;
|
||||
document.body.style.overflow = "";
|
||||
}, EASTER_FADE_MS);
|
||||
}
|
||||
|
||||
/* — Keyboard shortcuts ————————————————————————————————— */
|
||||
// T cycles forward, Shift+T cycles back, R picks random.
|
||||
document.addEventListener("keydown", (e) => {
|
||||
// While the easter egg is open, any key dismisses it (and only that).
|
||||
// While the easter egg is up, swallow every key — including T —
|
||||
// so the user can't trigger anything until it auto-dismisses.
|
||||
if (easterOpen) {
|
||||
e.preventDefault();
|
||||
hideEasterEgg();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -631,21 +655,24 @@ document.addEventListener("keydown", (e) => {
|
||||
|
||||
if (e.key === "t" || e.key === "T") {
|
||||
e.preventDefault();
|
||||
const order = Object.keys(THEMES);
|
||||
const i = order.indexOf(root.dataset.theme);
|
||||
const dir = e.shiftKey ? -1 : 1;
|
||||
const next = order[(i + dir + order.length) % order.length];
|
||||
applyTheme(next);
|
||||
|
||||
// Easter-egg counter — track press cadence in a rolling window.
|
||||
// Push BEFORE applying the theme so the trigger fires on this same
|
||||
// keystroke if we've crossed the threshold.
|
||||
const now = performance.now();
|
||||
while (tStamps.length && now - tStamps[0] > EASTER_WINDOW_MS) tStamps.shift();
|
||||
tStamps.push(now);
|
||||
if (tStamps.length >= EASTER_THRESHOLD && now - easterLastFired > EASTER_COOLDOWN_MS) {
|
||||
easterLastFired = now;
|
||||
showEasterEgg();
|
||||
tStamps.length = 0;
|
||||
showEasterEgg();
|
||||
return; // skip the theme swap on the trigger keystroke
|
||||
}
|
||||
|
||||
const order = Object.keys(THEMES);
|
||||
const i = order.indexOf(root.dataset.theme);
|
||||
const dir = e.shiftKey ? -1 : 1;
|
||||
const next = order[(i + dir + order.length) % order.length];
|
||||
applyTheme(next);
|
||||
} else if (e.key === "r" || e.key === "R") {
|
||||
e.preventDefault();
|
||||
applyTheme(pickRandomTheme());
|
||||
|
||||
Reference in New Issue
Block a user