From 435322be154ea2876cf69546ceeb8c03935cb1ad Mon Sep 17 00:00:00 2001 From: Labyricorn Date: Sun, 23 Aug 2026 08:56:42 -0700 Subject: [PATCH] Add listing page circuit heroes --- README.md | 27 ++ assets/static/section-heroes.js | 53 ++++ assets/static/style.css | 437 ++++++++++++++++++++++++++++++++ templates/base.html | 3 +- templates/section.html | 140 ++++++++++ 5 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 assets/static/section-heroes.js diff --git a/README.md b/README.md index 25c736a..6d2b52d 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,33 @@ README.md This runbook Generated output such as `build/`, `dist/`, and `.lektor/` is intentionally ignored and must not be committed. +## Top-level listing heroes + +The three primary listing routes each render one design-specific circuit hero +through `templates/section.html`: + +| Route | Hero treatment | +| --- | --- | +| `/projects/` | Green system-architecture and project-matrix display | +| `/articles/` | Cyan intelligence-briefing and knowledge-system display | +| `/blog/` | Pink transmission and live-signal display | + +These heroes apply only to the top-level Projects, Articles, and Blog listing +pages. Individual project pages, project devlogs, articles, and blog posts keep +their existing templates and headers. Each hero preserves its own SVG circuit +paths, status module, copy, timing, and responsive behavior while sharing +namespaced CSS in `assets/static/style.css`. The design declares a 175-pixel +minimum height rather than a fixed height, so its complete content determines +the final rendered height. At viewports up to 760 pixels wide, each hero's copy +and decorative panel use the design's single-column layout. + +`assets/static/section-heroes.js` supplies the circuit-packet and intermittent +glitch animations. The complete static hero remains available when JavaScript +or the Web Animations API is unavailable, and motion is disabled when the user +prefers reduced motion. When hero styles or behavior change, increment the +corresponding `style.css` or `section-heroes.js` cache-busting version in +`templates/base.html`. + ## Routine content workflow Edit in a normal Git checkout, review the changes, and push them to `main`. diff --git a/assets/static/section-heroes.js b/assets/static/section-heroes.js new file mode 100644 index 0000000..af35e5a --- /dev/null +++ b/assets/static/section-heroes.js @@ -0,0 +1,53 @@ +(() => { + const heroes = document.querySelectorAll("[data-section-hero]"); + if (!heroes.length) return; + + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (reducedMotion) return; + + heroes.forEach((hero) => { + const config = hero.classList.contains("section-hero--projects") + ? { pathPad: 22, duration: 2800, durationRange: 2200, delay: 250, delayRange: 1100, start: 250, stagger: 500, edgeOpacity: 0.18, liveOpacity: 0.8, glitch: 9000 } + : hero.classList.contains("section-hero--articles") + ? { pathPad: 20, duration: 2600, durationRange: 2200, delay: 300, delayRange: 1000, start: 250, stagger: 550, edgeOpacity: 0.15, liveOpacity: 0.78, glitch: 9800 } + : { pathPad: 20, duration: 2400, durationRange: 2300, delay: 250, delayRange: 900, start: 200, stagger: 520, edgeOpacity: 0.14, liveOpacity: 0.8, glitch: 7600 }; + + hero.querySelectorAll(".section-hero__packet").forEach((packet, index) => { + if (typeof packet.animate !== "function") return; + + const length = packet.getTotalLength(); + + const animate = () => { + const reverse = Math.random() > 0.5; + const from = reverse ? -(length + config.pathPad) : config.pathPad; + const to = reverse ? config.pathPad : -(length + config.pathPad); + const duration = config.duration + Math.random() * config.durationRange; + const animation = packet.animate( + [ + { strokeDashoffset: String(from), opacity: config.edgeOpacity }, + { offset: 0.15, opacity: config.liveOpacity }, + { offset: 0.85, opacity: config.liveOpacity }, + { strokeDashoffset: String(to), opacity: hero.classList.contains("section-hero--projects") ? 0.15 : 0.12 }, + ], + { duration, easing: "linear" }, + ); + + if (animation.finished) { + animation.finished + .then(() => window.setTimeout(animate, config.delay + Math.random() * config.delayRange)) + .catch(() => {}); + } else { + window.setTimeout(animate, duration + config.delay + Math.random() * config.delayRange); + } + }; + + window.setTimeout(animate, config.start + index * config.stagger); + }); + + window.setInterval(() => { + hero.classList.remove("is-glitching"); + void hero.offsetWidth; + hero.classList.add("is-glitching"); + }, config.glitch); + }); +})(); diff --git a/assets/static/style.css b/assets/static/style.css index 264b7c3..d2cc690 100644 --- a/assets/static/style.css +++ b/assets/static/style.css @@ -41,6 +41,422 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: .dek { max-width: 700px; margin-top: 18px; color: var(--muted); } .dek p { margin: 0; } +.section-hero { + --section-accent: #00f58b; + --section-accent-rgb: 0, 245, 139; + --section-accent-soft: #00c974; + --section-alt: #00f58b; + --section-bg: #050706; + --section-copy: #d4ddd8; + --section-muted: #8fa39a; + --section-trace: #0e5138; + --section-trace-soft: #123828; + --section-node: #158158; + position: relative; + width: min(100% - 40px, 1180px); + min-height: 175px; + margin: 32px auto 0; + overflow: hidden; + border: 1px solid rgba(var(--section-accent-rgb), .48); + background: + linear-gradient(rgba(var(--section-accent-rgb), .022) 1px, transparent 1px), + linear-gradient(90deg, rgba(var(--section-accent-rgb), .022) 1px, transparent 1px), + radial-gradient(circle at 78% 48%, rgba(var(--section-accent-rgb), .085), transparent 260px), + var(--section-bg); + background-size: 22px 22px, 22px 22px, auto, auto; + box-shadow: inset 0 0 36px rgba(var(--section-accent-rgb), .028); + isolation: isolate; +} +.section-hero--articles { + --section-accent: #19d7ff; + --section-accent-rgb: 25, 215, 255; + --section-accent-soft: #00aee8; + --section-alt: #ff37c7; + --section-bg: #040608; + --section-copy: #d8e0e4; + --section-muted: #8fa7b2; + --section-trace: #0f536a; + --section-trace-soft: #102f3b; + --section-node: #157995; +} +.section-hero--blog { + --section-accent: #ff2a9d; + --section-accent-rgb: 255, 42, 157; + --section-accent-soft: #d80f7e; + --section-alt: #7e36ff; + --section-bg: #050307; + --section-copy: #ded5dc; + --section-muted: #a992a5; + --section-trace: #5a173e; + --section-trace-soft: #301126; + --section-node: #8b235f; + background: + linear-gradient(rgba(var(--section-accent-rgb), .022) 1px, transparent 1px), + linear-gradient(90deg, rgba(var(--section-accent-rgb), .022) 1px, transparent 1px), + radial-gradient(circle at 78% 48%, rgba(var(--section-accent-rgb), .09), transparent 250px), + radial-gradient(circle at 66% 68%, rgba(126, 54, 255, .05), transparent 220px), + var(--section-bg); + background-size: 22px 22px, 22px 22px, auto, auto, auto; +} +.section-hero::after { + content: ""; + position: absolute; + z-index: 10; + inset: 0; + pointer-events: none; + opacity: .22; + background: repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0, rgba(255,255,255,.025) 1px, transparent 1px, transparent 4px); +} +.section-hero__corner { + position: absolute; + z-index: 12; + width: 42px; + height: 42px; +} +.section-hero__corner::before, +.section-hero__corner::after { + content: ""; + position: absolute; + background: var(--section-accent); + box-shadow: 0 0 8px rgba(var(--section-accent-rgb), .28); +} +.section-hero__corner::before { width: 32px; height: 2px; } +.section-hero__corner::after { width: 2px; height: 32px; } +.section-hero__corner--tl { top: 8px; left: 8px; } +.section-hero__corner--tr { top: 8px; right: 8px; transform: scaleX(-1); } +.section-hero__corner--bl { bottom: 8px; left: 8px; transform: scaleY(-1); } +.section-hero__corner--br { right: 8px; bottom: 8px; transform: scale(-1); } +.section-hero__content { + position: relative; + z-index: 4; + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(280px, .8fr); + gap: 28px; + min-height: 175px; + padding: 20px 36px; +} +.section-hero--articles .section-hero__content { grid-template-columns: minmax(0, 1.15fr) minmax(320px, .85fr); } +.section-hero--blog .section-hero__content { grid-template-columns: minmax(0, 1.14fr) minmax(320px, .86fr); } +.section-hero__copy { + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; +} +.section-hero__eyebrow { + display: flex; + align-items: center; + gap: 9px; + margin: 0 0 8px; + color: var(--section-accent); + font: 800 11px/1.2 var(--mono); + letter-spacing: .15em; + text-transform: uppercase; +} +.section-hero--projects .section-hero__eyebrow { margin-bottom: 10px; font-size: .72rem; letter-spacing: .16em; } +.section-hero--articles .section-hero__eyebrow, +.section-hero--blog .section-hero__eyebrow { font-size: .7rem; } +.section-hero--projects .section-hero__eyebrow::before { + content: ""; + width: 28px; + height: 2px; + background: var(--section-accent); + box-shadow: 0 0 7px rgba(var(--section-accent-rgb), .4); +} +.section-hero__pulse-dot, +.section-hero__live-dot { + display: inline-block; + width: 6px; + height: 6px; + flex: 0 0 auto; + border-radius: 50%; + background: var(--section-accent); + box-shadow: 0 0 10px rgba(var(--section-accent-rgb), .8); + animation: section-hero-blink 1.5s ease-in-out infinite; +} +.section-hero__doc-icon { + position: relative; + width: 19px; + height: 23px; + flex: 0 0 auto; + border: 1px solid var(--section-accent); + box-shadow: 0 0 7px rgba(var(--section-accent-rgb), .15); +} +.section-hero__doc-icon::before, +.section-hero__doc-icon::after { + content: ""; + position: absolute; + left: 4px; + height: 1px; + background: var(--section-accent); +} +.section-hero__doc-icon::before { top: 7px; width: 9px; } +.section-hero__doc-icon::after { top: 12px; width: 7px; } +.section-hero__chat-icon { + position: relative; + width: 24px; + height: 18px; + flex: 0 0 auto; + border: 1px solid var(--section-accent); + border-radius: 3px; + box-shadow: 0 0 8px rgba(var(--section-accent-rgb), .18); +} +.section-hero__chat-icon::before { + content: ""; + position: absolute; + top: 5px; + left: 4px; + width: 13px; + height: 1px; + background: var(--section-accent); + box-shadow: 0 4px 0 var(--section-accent); +} +.section-hero__chat-icon::after { + content: ""; + position: absolute; + bottom: -5px; + left: 4px; + border-width: 4px 5px 0 0; + border-style: solid; + border-color: var(--section-accent) transparent transparent transparent; +} +.section-hero h1 { + max-width: none; + margin: 0; + font: 900 clamp(2.65rem, 6vw, 4.8rem)/.82 Arial, Helvetica, sans-serif; + font-style: normal; + letter-spacing: .035em; + text-transform: uppercase; +} +.section-hero--articles h1, +.section-hero--blog h1 { letter-spacing: .04em; } +.section-hero__slash { + margin: 8px 0 0; + color: var(--section-accent); + font: 500 1.35rem/1.2 var(--mono); +} +.section-hero__slash span { + font-size: 1.05rem; + letter-spacing: .05em; + vertical-align: .12em; + text-transform: uppercase; +} +.section-hero--articles .section-hero__slash span, +.section-hero--blog .section-hero__slash span { font-size: 1.02rem; } +.section-hero__intro { + max-width: 600px; + margin: 10px 0 0; + color: var(--section-copy); + font: 1rem/1.6 Georgia, "Times New Roman", serif; +} +.section-hero--articles .section-hero__intro, +.section-hero--blog .section-hero__intro { font-size: .98rem; line-height: 1.45; } +.section-hero__chips { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} +.section-hero__chips > span { + border: 1px solid rgba(var(--section-accent-rgb), .22); + background: rgba(var(--section-accent-rgb), .025); + padding: .42rem .58rem; + color: var(--section-muted); + font: 500 .64rem/1.2 var(--mono); + letter-spacing: .07em; + text-transform: uppercase; +} +.section-hero--articles .section-hero__chips > span, +.section-hero--blog .section-hero__chips > span { padding: .38rem .52rem; font-size: .62rem; } +.section-hero__chips strong { color: var(--section-accent); } +.section-hero__visual { + position: relative; + min-height: 135px; + align-self: center; +} +.section-hero__visual svg { + display: block; + width: 100%; + height: 100%; + min-height: 135px; + overflow: visible; +} +.section-hero__trace, +.section-hero__trace-soft, +.section-hero__packet { + fill: none; + vector-effect: non-scaling-stroke; +} +.section-hero__trace { stroke: var(--section-trace); stroke-width: 1.05; } +.section-hero__trace-soft { stroke: var(--section-trace-soft); stroke-width: .9; } +.section-hero__packet { + stroke: var(--section-accent); + stroke-width: 1.3; + stroke-linecap: square; + stroke-dasharray: 16 9999; + opacity: .8; + filter: drop-shadow(0 0 4px rgba(var(--section-accent-rgb), .45)); +} +.section-hero__packet--alt { stroke: var(--section-alt); } +.section-hero--articles .section-hero__packet--alt { opacity: .5; } +.section-hero--blog .section-hero__packet--alt { opacity: .58; } +.section-hero__node { + fill: var(--section-bg); + stroke: var(--section-node); + stroke-width: 1; +} +.section-hero__status-card { + position: absolute; + top: 4px; + right: 4px; + width: 184px; + border: 1px solid rgba(var(--section-accent-rgb), .32); + background: rgba(3, 10, 15, .8); + padding: 9px 11px 8px; + backdrop-filter: blur(2px); +} +.section-hero--projects .section-hero__status-card { width: 170px; background: rgba(4, 10, 7, .8); } +.section-hero--articles .section-hero__status-card, +.section-hero--blog .section-hero__status-card { top: 2px; right: 2px; } +.section-hero--blog .section-hero__status-card { background: rgba(11, 3, 8, .8); } +.section-hero__widget-label { + display: block; + color: var(--section-accent); + font: 500 10px/1.2 var(--mono); + letter-spacing: .13em; + text-transform: uppercase; +} +.section-hero--projects .section-hero__widget-label { font-size: .65rem; letter-spacing: .14em; } +.section-hero__status-card > strong { + display: block; + margin-top: 6px; + font: 800 18px/1.2 var(--mono); + letter-spacing: .08em; + text-transform: uppercase; +} +.section-hero--projects .section-hero__status-card > strong { font-size: 1.15rem; } +.section-hero__bars { display: flex; gap: 4px; margin-top: 7px; } +.section-hero__bars i { + width: 14px; + height: 8px; + border: 1px solid var(--section-accent-soft); + background: var(--section-accent); + box-shadow: 0 0 6px rgba(var(--section-accent-rgb), .35); +} +.section-hero__bars i.is-off { background: transparent; box-shadow: none; opacity: .45; } +.section-hero__status-line { + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 7px; + color: var(--section-copy); + font: 500 10px/1.2 var(--mono); + letter-spacing: .04em; + text-transform: uppercase; +} +.section-hero__status-line b { color: var(--section-accent); } +.section-hero__radar, +.section-hero__signal-core { + position: absolute; + top: 7px; + right: 196px; + width: 105px; + height: 105px; + border: 1px solid rgba(var(--section-accent-rgb), .18); + border-radius: 50%; +} +.section-hero__radar { + background: + radial-gradient(circle, transparent 0 24%, rgba(var(--section-accent-rgb), .09) 24.5% 25.5%, transparent 26% 48%, rgba(var(--section-accent-rgb), .07) 48.5% 49.5%, transparent 50%), + linear-gradient(90deg, transparent 49.5%, rgba(var(--section-accent-rgb), .14) 50%, transparent 50.5%), + linear-gradient(transparent 49.5%, rgba(var(--section-accent-rgb), .14) 50%, transparent 50.5%); + opacity: .82; +} +.section-hero__radar::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 46%; + height: 1px; + background: linear-gradient(90deg, rgba(var(--section-accent-rgb), .85), transparent); + box-shadow: 0 0 7px rgba(var(--section-accent-rgb), .35); + transform-origin: left center; + animation: section-hero-sweep 3.4s linear infinite; +} +.section-hero__signal-core { + top: 9px; + right: 205px; + width: 102px; + height: 102px; + background: + radial-gradient(circle at 50% 50%, rgba(var(--section-accent-rgb), .22) 0 14%, transparent 15%), + radial-gradient(circle, transparent 0 38%, rgba(var(--section-accent-rgb), .08) 39% 40%, transparent 41%), + linear-gradient(90deg, transparent 49.5%, rgba(var(--section-accent-rgb), .12) 50%, transparent 50.5%), + linear-gradient(transparent 49.5%, rgba(var(--section-accent-rgb), .12) 50%, transparent 50.5%); + box-shadow: 0 0 18px rgba(var(--section-accent-rgb), .08); +} +.section-hero__signal-core::before { + content: ""; + position: absolute; + inset: 20px; + background: var(--section-accent); + clip-path: polygon(0 44%, 38% 44%, 46% 20%, 57% 76%, 66% 40%, 100% 40%, 100% 58%, 72% 58%, 58% 92%, 47% 48%, 40% 62%, 0 62%); + filter: drop-shadow(0 0 5px rgba(var(--section-accent-rgb), .55)); + animation: section-hero-pulse 1.7s ease-in-out infinite; +} +.section-hero__signal-row { + position: absolute; + right: 18px; + bottom: 1px; + display: flex; + justify-content: space-between; + gap: 14px; + width: 270px; + margin: 0; + border-top: 1px solid rgba(var(--section-accent-rgb), .2); + padding-top: 7px; + color: var(--section-muted); + font: 500 .59rem/1.4 var(--mono); + letter-spacing: .04em; + text-transform: uppercase; +} +.section-hero--articles .section-hero__signal-row { right: 22px; width: 255px; } +.section-hero__signal-row b { color: var(--section-accent); } +.section-hero__signal-row--stacked { + right: 18px; + bottom: 2px; + display: block; + width: 214px; + line-height: 1.5; + font-size: .64rem; +} +.section-hero__glitch-strip { + position: absolute; + top: 42%; + right: 5%; + left: 47%; + height: 2px; + opacity: .22; + background: linear-gradient(90deg, transparent 0 8%, var(--section-accent) 8% 20%, transparent 20% 28%, var(--section-alt) 28% 36%, transparent 36% 65%, var(--section-accent) 65% 81%, transparent 81%); + animation: section-hero-strip 2.8s steps(3, end) infinite; +} +.section-hero.is-glitching { animation: section-hero-glitch .34s steps(2, end); } +.section-hero--articles.is-glitching { animation-duration: .32s; } + +@keyframes section-hero-blink { 0%, 100% { opacity: .35; } 50% { opacity: 1; } } +@keyframes section-hero-sweep { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +@keyframes section-hero-pulse { 0%, 100% { opacity: .42; transform: scaleX(.92); } 50% { opacity: 1; transform: scaleX(1.05); } } +@keyframes section-hero-strip { 0%, 100% { transform: translateX(0); opacity: .12; } 40% { transform: translateX(8px); opacity: .28; } 65% { transform: translateX(-5px); opacity: .18; } } +@keyframes section-hero-glitch { + 0% { transform: translate(0); filter: none; } + 20% { transform: translate(3px, -1px); filter: contrast(1.3) saturate(1.2); } + 40% { transform: translate(-5px, 1px); } + 60% { transform: translate(2px, 0); filter: hue-rotate(7deg); } + 80% { transform: translate(-2px, -1px); } + 100% { transform: translate(0); filter: none; } +} + .circuit-hero { --circuit-gold: #c5a059; --circuit-cyan: #65f7ff; @@ -768,6 +1184,21 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: .footer-inner { min-height: 105px; display: flex; justify-content: space-between; align-items: center; gap: 24px; } .site-footer nav { display: flex; gap: 22px; } +@media (max-width: 760px) { + .section-hero { width: calc(100% - 28px); margin-top: 20px; } + .section-hero__content { grid-template-columns: 1fr; padding: 24px 22px 20px; } + .section-hero h1 { font-size: clamp(42px, 13vw, 68px); } + .section-hero__visual { min-height: 125px; } + .section-hero--articles .section-hero__visual, + .section-hero--blog .section-hero__visual { min-height: 128px; } + .section-hero__radar, + .section-hero__signal-core { top: 7px; right: auto; left: 0; width: 92px; height: 92px; } + .section-hero__status-card { right: 0; } + .section-hero__signal-row { right: 0; width: 100%; max-width: 290px; } + .section-hero__signal-row--stacked { right: 10px; width: 214px; } + .section-hero__glitch-strip { left: 30%; } +} + @media (max-width: 860px) and (min-width: 721px) { .circuit-hero { height: 720px; min-height: 720px; } .circuit-brand-block { width: 72vw; } @@ -882,6 +1313,12 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: @media (prefers-reduced-motion: reduce) { .circuit-status span, .circuit-node-main i, + .section-hero, + .section-hero__pulse-dot, + .section-hero__live-dot, + .section-hero__radar::after, + .section-hero__signal-core::before, + .section-hero__glitch-strip, .share-action:hover::after, .share-action:focus-visible::after { animation: none; } } diff --git a/templates/base.html b/templates/base.html index 7c3e72c..e15aa1c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,10 +10,11 @@ - + + diff --git a/templates/section.html b/templates/section.html index a417519..69a24f1 100644 --- a/templates/section.html +++ b/templates/section.html @@ -10,12 +10,152 @@ {% endfor %} {% endfor %} {% endif %} +{% if this._id in ['blog', 'articles', 'projects'] %} +{% set hero_copy = { + 'projects': { + 'eyebrow': 'System Architecture', + 'slash': 'Built to solve', + 'intro': 'Systems, tools, experiments, and working software engineered from idea through implementation.', + 'chips': [('Modular', 'architecture'), ('Active', 'development'), ('Documented', 'progress')] + }, + 'articles': { + 'eyebrow': 'Intelligence Briefing', + 'slash': 'Analysis & Insight', + 'intro': 'In-depth analysis, real context, and actionable insight across technology, systems, security, and the work behind the work.', + 'chips': [('Curated', 'research'), ('Verified', 'context'), ('Published', 'analysis')] + }, + 'blog': { + 'eyebrow': 'Transmission / Live Feed', + 'slash': 'Signal', + 'intro': 'Raw thoughts, quick observations, experiments, and ideas transmitted before they become polished conclusions.', + 'chips': [('Live', 'thoughts'), ('Direct', 'signal'), ('Unfiltered', 'notes')] + } +} %} +{% set hero = hero_copy[this._id] %} +
+ + + + + {% if this._id == 'blog' %}{% endif %} + +
+
+

+ {% if this._id == 'projects' %} + + {% elif this._id == 'articles' %} + + {% else %} + + {% endif %} + {{ hero.eyebrow }} + {% if this._id == 'blog' %}{% endif %} +

+

{{ this.title }}

+ +

{{ hero.intro }}

+
+ {% for lead, rest in hero.chips %}{{ lead }} {{ rest }}{% endfor %} +
+
+ + +
+
+{% else %}

{{ this.eyebrow }}

{{ this.summary }}

+{% endif %}
{{ this.title }}{{ this.children.count() }} entries