style(about): redesign /about as numbered "Liner Notes" editorial spine

Replace Home-cloned section grammar with a numbered left rail (Bodoni
numerals, vertical spine, mono marginalia), an asymmetric content column,
and SVG waveform dividers. Adds a degrade-safe IntersectionObserver interop.
Copy verbatim.
This commit is contained in:
daniel-c-harvey
2026-06-17 20:04:00 -04:00
parent a210b2ded7
commit c8168564bb
3 changed files with 701 additions and 529 deletions
+249 -208
View File
@@ -1,9 +1,27 @@
@page "/about" @page "/about"
@using DeepDrftPublic.Client.Controls @using DeepDrftPublic.Client.Controls
@implements IAsyncDisposable
@inject IJSRuntime JsRuntime
<PageTitle>The Collective - Deep DRFT</PageTitle> <PageTitle>The Collective - Deep DRFT</PageTitle>
@* ── HERO (split 50/50) — reuses the .hero-* type classes with About's own words. @* ──────────────────────────────────────────────────────────────────────────────
THE LINER NOTES — a numbered three-movement editorial essay.
This page deliberately does NOT reuse Home's section grammar (centred dividers,
symmetric 4/8 splits, the medium-card grid). Its backbone is a persistent left
"rail" — a continuous vertical hairline (the narrative spine) carrying oversized
Bodoni movement numerals (01/02/03) and mono marginalia — with the content column
running asymmetrically to its right. Movement boundaries are rendered as a
self-contained SVG waveform stroke (the DeepDrft visualizer motif, hand-authored
here — NOT the live WaveformVisualizer component).
The numeral active-highlight (green on the movement in view) is progressive
enhancement via IntersectionObserver: without JS the numerals still render
statically in low-opacity navy. See Interop/about/about-rail.ts.
────────────────────────────────────────────────────────────────────────────── *@
@* ── HERO — the page opener. Reuses the .hero-* type scale with About's own words.
NOT DeepDrftHero (that hard-codes the Deep/DRFT masthead + streaming CTA). ── *@ NOT DeepDrftHero (that hard-codes the Deep/DRFT masthead + streaming CTA). ── *@
<section class="hero"> <section class="hero">
<MudGrid Spacing="0" Style="height: 100%;"> <MudGrid Spacing="0" Style="height: 100%;">
@@ -18,7 +36,7 @@
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
@* IMG SLOT A — hero duo portrait. Reuses the committed duo pair as interim. *@ @* IMG SLOT A — hero duo portrait, inset within the content column. *@
<div class="hero-image-pane"> <div class="hero-image-pane">
<ParallaxImage Image1="img/dd-duo-2-bw.jpg" <ParallaxImage Image1="img/dd-duo-2-bw.jpg"
Image2="img/dd-duo-2.jpeg" Image2="img/dd-duo-2.jpeg"
@@ -32,55 +50,43 @@
</div> </div>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
@* IMG SLOT B — full-bleed band under the hero, bw+colour crossfade pair. *@
<ParallaxImage Image1="img/dd-duo-hero-bw.jpeg"
Image2="img/dd-duo-hero.jpeg"
Alt1="Deep DRFT Electronic Music Duo"
FullWidth
InvertDirection
NaturalWidth="2048"
NaturalHeight="1365"
WindowHeightFraction="0.45"
ImageHeight="auto"
ImageWidth="100%"
ParallaxSpeed="0.35"
Class="my-12" />
</section> </section>
@* ════════════════════ MOVEMENT ONE — THE PEOPLE (pathos) ════════════════════ *@ @* ════════════════ MOVEMENT ONE — THE PEOPLE (pathos) ════════════════ *@
<div class="section-divider"> <div class="movement" data-movement="1" @ref="_movementOne">
<div class="divider-line"></div> <div class="rail" aria-hidden="true">
<div class="divider-tag">The People</div> <div class="rail-line"></div>
<div class="divider-line"></div> <div class="rail-numeral">01</div>
</div> <div class="rail-margin">Charleston, SC</div>
@* People intro — two-column section: label + serif title left, prose right. *@
<section class="section">
<div class="section-header-grid">
<MudGrid Style="margin-bottom: 5rem;">
<MudItem xs="12" md="4">
<div class="section-label">The Collective</div>
<h2 class="section-title">Two of Us,<br />No Fixed<br /><em>Roles</em></h2>
</MudItem>
<MudItem xs="12" md="8" Class="section-body-item">
<div class="section-body">
<p>
We met trading synthesizers and found out we were seeking the same thing. Two of us, no fixed roles &mdash; we both write, arrange, produce, mix, record in the field, build the visuals, and make the tools when the tools don't exist yet.
</p>
</div>
</MudItem>
</MudGrid>
</div> </div>
@* Member bio pair — two cards side by side; each composes with the body absent <div class="movement-content">
(Khabran ships with an empty body slot, same null-renders-nothing discipline @* Waveform movement divider — a static SVG oscillation stroke carrying the
as ReleaseDescription). *@ movement tag. The folded-in D3 signature motif. *@
<MudGrid> <div class="wave-divider">
@foreach (var member in _members) <svg class="wave-stroke" viewBox="0 0 1200 40" preserveAspectRatio="none" aria-hidden="true">
{ <path d="@WavePath" />
<MudItem xs="12" md="6"> </svg>
<div class="bio-card"> <span class="wave-tag">The People</span>
</div>
@* People intro — prose hangs at the rail's left edge; the sharp line breaks
left into the margin at large serif scale. *@
<div class="movement-intro">
<div class="movement-label">The Collective</div>
<h2 class="movement-title">Two of Us, No Fixed <em>Roles</em></h2>
<p class="movement-prose">
We met trading synthesizers and found out we were seeking the same thing. Two of us, no fixed roles &mdash; we both write, arrange, produce, mix, record in the field, build the visuals, and make the tools when the tools don't exist yet.
</p>
</div>
@* Member bio pair — framed portrait insets with rail-side captions. Each
composes with the body absent (Khabran ships with an empty body slot, the
same null-renders-nothing discipline as ReleaseDescription). *@
<div class="bio-pair">
@foreach (var member in _members)
{
<article class="bio-card">
<div class="bio-portrait"> <div class="bio-portrait">
@if (member.PortraitImage1 is not null) @if (member.PortraitImage1 is not null)
{ {
@@ -100,170 +106,150 @@
<div class="bio-portrait-placeholder" aria-hidden="true"></div> <div class="bio-portrait-placeholder" aria-hidden="true"></div>
} }
</div> </div>
<div class="bio-caption">@member.Name &middot; @member.Role</div>
<div class="bio-meta"> <div class="bio-meta">
<div class="bio-name">@member.Name</div> <div class="bio-name">@member.Name</div>
<div class="bio-role">@member.Role</div>
@if (!string.IsNullOrWhiteSpace(member.Bio)) @if (!string.IsNullOrWhiteSpace(member.Bio))
{ {
<p class="bio-body">@member.Bio</p> <p class="bio-body">@member.Bio</p>
} }
</div> </div>
</div> </article>
</MudItem> }
} </div>
</MudGrid> </div>
</section>
@* ════════════════════ MOVEMENT TWO — THE PROCESS (logos) ════════════════════ *@
<div class="section-divider">
<div class="divider-line"></div>
<div class="divider-tag">The Process</div>
<div class="divider-line"></div>
</div> </div>
@* Dark feature band — gear-stage cards. The dark ground carries the analytical register. *@ @* ════════════════ MOVEMENT TWO — THE PROCESS (logos) ════════════════ *@
<section class="section-dark"> <div class="movement" data-movement="2" @ref="_movementTwo">
<div class="section-label section-label-dark">How It's Made</div> <div class="rail" aria-hidden="true">
<h2 class="section-title section-title-dark"> <div class="rail-line"></div>
Digital, Analog,<br /><em>Whatever Moves</em> <div class="rail-numeral">02</div>
</h2> <div class="rail-margin">the live rig</div>
<p class="section-dark-standfirst">
It doesn't matter how &mdash; digital or analog, hard or soft, bought or built &mdash; as long as it moves the room. The soul in this music is designed, not extracted; assembled, not distilled.
</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" /><line x1="3" y1="9" x2="21" y2="9" /><line x1="9" y1="9" x2="9" y2="21" /></svg>
</div>
<div class="feature-title">Sketch</div>
<div class="feature-desc">A loop starts on the Force or the MPC, hands on the pads. The idea has to survive first contact before anything else gets built around it.</div>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><line x1="4" y1="21" x2="4" y2="14" /><line x1="4" y1="10" x2="4" y2="3" /><line x1="12" y1="21" x2="12" y2="12" /><line x1="12" y1="8" x2="12" y2="3" /><line x1="20" y1="21" x2="20" y2="16" /><line x1="20" y1="12" x2="20" y2="3" /><line x1="1" y1="14" x2="7" y2="14" /><line x1="9" y1="8" x2="15" y2="8" /><line x1="17" y1="16" x2="23" y2="16" /></svg>
</div>
<div class="feature-title">Arrange</div>
<div class="feature-desc">Sometimes into Ableton, sometimes start-to-finish in REAPER. The track gets shaped wherever it wants to go &mdash; we follow the take, not the template.</div>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="3" /><line x1="12" y1="3" x2="12" y2="6" /><line x1="12" y1="18" x2="12" y2="21" /></svg>
</div>
<div class="feature-title">Studio</div>
<div class="feature-desc">A deep bench of synths, drum machines, and pedals; digital and analog, hard and soft, some of it built by hand. If the sound we need doesn't exist yet, we make the thing that makes it.</div>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><path d="M3 12h3l2-7 4 14 2-7h3" /><circle cx="20" cy="12" r="1.5" /></svg>
</div>
<div class="feature-title">Live Rig</div>
<div class="feature-desc">No laptop, no safety net. A full spread of hardware patched together and played 100% live &mdash; sequenced, twisted, and pushed in the moment. Built for the room, the warehouse, the night that doesn't repeat.</div>
</div>
</div> </div>
</section>
@* IMG SLOT D — hands-on-gear band, the literal proof-of-effort image. *@ <div class="movement-content">
<section> <div class="wave-divider">
<ParallaxImage Image1="img/mixer-bw.jpg" <svg class="wave-stroke" viewBox="0 0 1200 40" preserveAspectRatio="none" aria-hidden="true">
Image2="img/mixer.jpg" <path d="@WavePath" />
Alt1="Deep DRFT — hands on the gear" </svg>
NaturalWidth="2048" <span class="wave-tag">The Process</span>
NaturalHeight="1365" </div>
WindowHeightFraction="0.45"
ImageHeight="auto"
ImageWidth="100%"
ParallaxSpeed="0.35" />
</section>
@* ════════════════════ MOVEMENT THREE — THE PRODUCT (ethos) ════════════════════ *@ @* Dark band — gear-stage cards. The navy ground carries the analytical register. *@
<div class="section-divider"> <div class="process-band">
<div class="divider-line"></div> <div class="process-label">How It's Made</div>
<div class="divider-tag">The Product</div> <h2 class="process-title">Digital, Analog, <em>Whatever Moves</em></h2>
<div class="divider-line"></div>
<p class="process-standfirst">
It doesn't matter how &mdash; digital or analog, hard or soft, bought or built &mdash; as long as it moves the room. The soul in this music is designed, not extracted; assembled, not distilled.
</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" /><line x1="3" y1="9" x2="21" y2="9" /><line x1="9" y1="9" x2="9" y2="21" /></svg>
</div>
<div class="feature-title">Sketch</div>
<div class="feature-desc">A loop starts on the Force or the MPC, hands on the pads. The idea has to survive first contact before anything else gets built around it.</div>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><line x1="4" y1="21" x2="4" y2="14" /><line x1="4" y1="10" x2="4" y2="3" /><line x1="12" y1="21" x2="12" y2="12" /><line x1="12" y1="8" x2="12" y2="3" /><line x1="20" y1="21" x2="20" y2="16" /><line x1="20" y1="12" x2="20" y2="3" /><line x1="1" y1="14" x2="7" y2="14" /><line x1="9" y1="8" x2="15" y2="8" /><line x1="17" y1="16" x2="23" y2="16" /></svg>
</div>
<div class="feature-title">Arrange</div>
<div class="feature-desc">Sometimes into Ableton, sometimes start-to-finish in REAPER. The track gets shaped wherever it wants to go &mdash; we follow the take, not the template.</div>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="3" /><line x1="12" y1="3" x2="12" y2="6" /><line x1="12" y1="18" x2="12" y2="21" /></svg>
</div>
<div class="feature-title">Studio</div>
<div class="feature-desc">A deep bench of synths, drum machines, and pedals; digital and analog, hard and soft, some of it built by hand. If the sound we need doesn't exist yet, we make the thing that makes it.</div>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24"><path d="M3 12h3l2-7 4 14 2-7h3" /><circle cx="20" cy="12" r="1.5" /></svg>
</div>
<div class="feature-title">Live Rig</div>
<div class="feature-desc">No laptop, no safety net. A full spread of hardware patched together and played 100% live &mdash; sequenced, twisted, and pushed in the moment. Built for the room, the warehouse, the night that doesn't repeat.</div>
</div>
</div>
</div>
@* IMG SLOT D — hands-on-gear inset, the literal proof-of-effort image,
captioned in the rail rather than run full-bleed. *@
<figure class="movement-figure">
<ParallaxImage Image1="img/mixer-bw.jpg"
Image2="img/mixer.jpg"
Alt1="Deep DRFT — hands on the gear"
NaturalWidth="2048"
NaturalHeight="1365"
WindowHeightFraction="0.45"
ImageHeight="auto"
ImageWidth="100%"
ParallaxSpeed="0.35" />
<figcaption class="figure-caption">the live rig</figcaption>
</figure>
</div>
</div> </div>
@* Product intro — two-column section framing the catalogue as evidence. *@ @* ════════════════ MOVEMENT THREE — THE PRODUCT (ethos) ════════════════ *@
<section class="section"> <div class="movement" data-movement="3" @ref="_movementThree">
<div class="section-header-grid"> <div class="rail" aria-hidden="true">
<MudGrid Style="margin-bottom: 5rem;"> <div class="rail-line"></div>
<MudItem xs="12" md="4"> <div class="rail-numeral">03</div>
<div class="section-label">The Output</div> <div class="rail-margin">in the swamp</div>
<h2 class="section-title">Classics,<br />with a<br /><em>Twist</em></h2>
</MudItem>
<MudItem xs="12" md="8" Class="section-body-item">
<div class="section-body">
<p>
Everything ends up here, in the catalogue. It's proof people in Charleston are pushing the sound of the club.
</p>
</div>
</MudItem>
</MudGrid>
</div> </div>
@* Medium triptych — one-line frame of each medium; definitions, not a re-pitch. *@ <div class="movement-content">
<div class="medium-grid"> <div class="wave-divider">
<a class="medium-card" href="/cuts"> <svg class="wave-stroke" viewBox="0 0 1200 40" preserveAspectRatio="none" aria-hidden="true">
<div class="medium-image" style="background-image: url('img/dd-studio.jpg');"> <path d="@WavePath" />
<div class="medium-scrim"></div> </svg>
</div> <span class="wave-tag">The Product</span>
<div class="medium-body"> </div>
<div class="medium-type">Studio</div>
<div class="medium-name">Cuts</div> <div class="movement-intro">
<div class="medium-desc">Studio work, composed and finished.</div> <div class="movement-label">The Output</div>
</div> <h2 class="movement-title">Classics, with a <em>Twist</em></h2>
</a> <p class="movement-prose">
<a class="medium-card" href="/sessions"> Everything ends up here, in the catalogue. It's proof people in Charleston are pushing the sound of the club.
<div class="medium-image" style="background-image: url('img/dd-live.jpeg');"> </p>
<div class="medium-scrim"></div> </div>
</div>
<div class="medium-body"> @* Medium triptych — one-line frame of each medium; definitions, not a re-pitch.
<div class="medium-type">Live</div> A stacked editorial list rather than Home's card grid. *@
<div class="medium-name">Sessions</div> <ul class="medium-list">
<div class="medium-desc">Live, caught once, never the same twice.</div> <li class="medium-row">
</div> <a href="/cuts">
</a> <span class="medium-row-name">Cuts</span>
<a class="medium-card" href="/mixes"> <span class="medium-row-desc">Studio work, composed and finished.</span>
<div class="medium-image" style="background-image: url('img/dd-dj.jpeg');"> </a>
<div class="medium-scrim"></div> </li>
</div> <li class="medium-row">
<div class="medium-body"> <a href="/sessions">
<div class="medium-type">DJ Set</div> <span class="medium-row-name">Sessions</span>
<div class="medium-name">Mixes</div> <span class="medium-row-desc">Live, caught once, never the same twice.</span>
<div class="medium-desc">Uninterrupted sets, start to finish.</div> </a>
</div> </li>
</a> <li class="medium-row">
<a href="/mixes">
<span class="medium-row-name">Mixes</span>
<span class="medium-row-desc">Uninterrupted sets, start to finish.</span>
</a>
</li>
</ul>
@* The live turn — "on the street, in the swamp": the identity beyond releases.
A left-breaking pull-quote at large serif scale. *@
<blockquote class="pull-quote">
<span class="pull-eyebrow">Beyond the Releases</span>
<p>
But that's just the releases. We're also out there &mdash; on the street, in the swamp, with a PA, a generator, and a bunch of good vibes.
</p>
</blockquote>
</div> </div>
</section>
@* The live turn — "on the street, in the swamp": the identity beyond the releases. *@
<div class="section-split">
<MudGrid Spacing="0" Style="height: 100%;">
<MudItem xs="12" md="6">
<div class="split-left">
<div class="split-eyebrow">Beyond the Releases</div>
<h2 class="split-title">On the Street,<br /><em>in the Swamp</em></h2>
<p class="split-body">
But that's just the releases. We're also out there &mdash; on the street, in the swamp, with a PA, a generator, and a bunch of good vibes.
</p>
</div>
</MudItem>
<MudItem xs="12" md="6">
<div class="split-right">
<ParallaxImage Image1="img/dd-live.jpeg"
Alt1="Deep DRFT — live, out in the world"
InvertDirection
NaturalWidth="2048"
NaturalHeight="1365"
WindowHeightFraction="0.7"
ImageHeight="auto"
ImageWidth="100%"
ParallaxSpeed="0.6" />
</div>
</MudItem>
</MudGrid>
</div> </div>
@* ── Closing CTA into the catalogue ── *@ @* ── Closing CTA into the catalogue ── *@
@@ -278,23 +264,78 @@
</div> </div>
</section> </section>
@* IMG SLOT E — closing atmosphere band. *@
<section>
<ParallaxImage Image1="img/dd-duo-hero-bw.jpeg"
Image2="img/dd-duo-hero.jpeg"
Alt1="Deep DRFT"
NaturalWidth="2048"
NaturalHeight="1365"
WindowHeightFraction="0.45"
InvertDirection
ImageHeight="auto"
ImageWidth="100%"
ParallaxSpeed="0.35" />
</section>
@code { @code {
private string AnimClass => RendererInfo.IsInteractive ? string.Empty : "fade-up"; private string AnimClass => RendererInfo.IsInteractive ? string.Empty : "fade-up";
// A static sine path for the movement-divider waveform stroke. Authored as plain
// SVG markup — independent of the live WaveformVisualizer component. The viewBox is
// 1200×40; the curve oscillates around the vertical midline (y=20).
private static readonly string WavePath = BuildWavePath();
private ElementReference _movementOne;
private ElementReference _movementTwo;
private ElementReference _movementThree;
private IJSObjectReference? _railModule;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || !RendererInfo.IsInteractive)
{
return;
}
try
{
// Progressive enhancement only: lights the active movement's numeral green
// as it scrolls into view. Numerals render statically without this.
_railModule = await JsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/about/about-rail.js");
await _railModule.InvokeVoidAsync("observe", _movementOne, _movementTwo, _movementThree);
}
catch (JSException)
{
// Module failed to load — numerals stay statically navy. Nothing actionable.
_railModule = null;
}
}
public async ValueTask DisposeAsync()
{
if (_railModule is not null)
{
try
{
await _railModule.InvokeVoidAsync("unobserve");
await _railModule.DisposeAsync();
}
catch (JSException)
{
// Runtime already gone (navigation/teardown) — nothing to clean up.
}
_railModule = null;
}
}
// Builds an evaluated-at-compile-time sine path string for the divider stroke.
private static string BuildWavePath()
{
// 8 full cycles across the 1200-wide viewBox, amplitude 14 around midline 20.
const int width = 1200;
const int steps = 96;
const double midline = 20;
const double amplitude = 14;
const double cycles = 8;
var sb = new System.Text.StringBuilder("M 0 20");
for (var i = 1; i <= steps; i++)
{
var x = width * (double)i / steps;
var y = midline - amplitude * System.Math.Sin(cycles * 2 * System.Math.PI * i / steps);
sb.Append(System.Globalization.CultureInfo.InvariantCulture, $" L {x:0.##} {y:0.##}");
}
return sb.ToString();
}
// Member bios. Khabran's body is an intentional empty slot — the card composes // Member bios. Khabran's body is an intentional empty slot — the card composes
// without it (graceful degrade). Daniel's copy is verbatim per spec COPY C, // without it (graceful degrade). Daniel's copy is verbatim per spec COPY C,
// including the two typos he chose to keep ("embarked in", "metalhead at from"). // including the two typos he chose to keep ("embarked in", "metalhead at from").
+387 -321
View File
@@ -1,12 +1,16 @@
/* About.razor scoped styles. /* About.razor scoped styles — "The Liner Notes".
The About page is built entirely in the Home page's visual language. Those section This page diverges from Home by composition, not vocabulary. The backbone is a
primitives (.hero-*, .section, .section-divider, .section-dark, .section-split, persistent left RAIL (a continuous vertical hairline carrying oversized Bodoni
.medium-*, .cta-*, .feature-*) live SCOPED in Home.razor.css and DeepDrftHero.razor.css movement numerals + mono marginalia) with the content column offset asymmetrically
— they are not in the global stylesheet, so Blazor CSS isolation will not share them to its right. Movement boundaries are rendered as a hand-authored SVG waveform
with this component. The primitives are therefore re-declared here, verbatim from those stroke (the D3 motif folded in). Palette tokens, type stack, the dark Process band,
sources, so About renders identically without touching Home's rendering surface. The only the feature-card grid, the CTA, and the bw↔colour ParallaxImage crossfade are all
genuinely new styling is the two bio-card pieces (.bio-*) at the end. */ reused from the site's existing vocabulary — only the structure is new.
Home's borrowed primitives that this redesign supersedes (.section-divider /
.divider-line centred rules, the symmetric .section-header-grid 4/8 split, the
.medium-card grid, .section-split) are intentionally NOT re-declared here. */
/* ── Animations (from DeepDrftHero.razor.css) ── */ /* ── Animations (from DeepDrftHero.razor.css) ── */
@keyframes fade-up { @keyframes fade-up {
@@ -19,7 +23,7 @@
animation: fade-up 0.8s ease forwards; animation: fade-up 0.8s ease forwards;
} }
/* ── HERO (from Home.razor.css + DeepDrftHero.razor.css) ── */ /* ── HERO — the page opener (type scale from Home's .hero-*) ── */
.hero { .hero {
min-height: 100vh; min-height: 100vh;
overflow: hidden; overflow: hidden;
@@ -96,43 +100,252 @@
animation-delay: 0.44s; animation-delay: 0.44s;
} }
/* ── DIVIDER (from Home.razor.css) ── */ /* ══════════════════ THE RAIL + SPINE — the signature device ══════════════════
.section-divider {
display: flex; Each movement is a two-column grid: a narrow rail column on the left carrying the
align-items: center; continuous vertical hairline (the narrative spine), the oversized Bodoni numeral,
gap: 2rem; and the mono marginalia; the content column to its right. The rail column is
padding: 2rem 3rem; ~14% of the page on desktop and collapses to an inline header on mobile. */
.movement {
display: grid;
grid-template-columns: minmax(140px, 14%) minmax(0, 1fr);
background: var(--deepdrft-white); background: var(--deepdrft-white);
align-items: start;
} }
.divider-line { .rail {
flex: 1; position: relative;
height: 1px; align-self: stretch;
padding: 4rem 0 4rem 3rem;
}
/* The continuous vertical hairline — Home's horizontal .divider-line reoriented,
the same --deepdrft-border token, running the length of the movement. */
.rail-line {
position: absolute;
top: 0;
bottom: 0;
left: 3rem;
width: 1px;
background: var(--deepdrft-border); background: var(--deepdrft-border);
} }
.divider-tag { /* Oversized Bodoni movement numeral. Sticks near the top of the viewport as the
movement scrolls, low-opacity navy by default; the active movement lights green
(toggled by the IntersectionObserver — see Interop/about/about-rail.ts). */
.rail-numeral {
position: sticky;
top: 6rem;
font-family: var(--deepdrft-font-display);
font-size: clamp(5rem, 10vw, 9rem);
font-weight: 300;
line-height: 1;
letter-spacing: -0.04em;
color: var(--deepdrft-navy);
opacity: 0.14;
padding-left: 1.4rem;
transition: color 0.5s ease, opacity 0.5s ease;
}
.movement.is-active .rail-numeral {
color: var(--deepdrft-green-accent);
opacity: 0.95;
}
/* Mono marginalia — a rotated caption set against the spine, the way a magazine
annotates a photo. Reuses the mono eyebrow idiom. */
.rail-margin {
position: sticky;
top: 16rem;
margin-top: 2rem;
padding-left: 1.4rem;
font-family: var(--deepdrft-font-mono); font-family: var(--deepdrft-font-mono);
font-size: 0.6rem; font-size: 0.58rem;
letter-spacing: 0.25em; letter-spacing: 0.24em;
color: var(--deepdrft-muted);
text-transform: uppercase; text-transform: uppercase;
color: var(--deepdrft-muted);
writing-mode: vertical-rl;
transform: rotate(180deg);
transform-origin: center;
height: 12rem;
}
/* ── The content column — asymmetric, left-anchored prose ── */
.movement-content {
padding: 4rem 3rem 5rem 1rem;
min-width: 0;
}
/* ══════════════════ WAVEFORM MOVEMENT DIVIDER (D3 motif) ══════════════════
A self-contained SVG oscillation stroke with the mono movement tag sitting on it.
Replaces Home's flat .divider-line rule between movements. */
.wave-divider {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 4rem;
}
.wave-stroke {
flex: 1;
height: 28px;
min-width: 0;
overflow: visible;
}
.wave-stroke path {
fill: none;
stroke: var(--deepdrft-green-accent);
stroke-width: 1.4;
opacity: 0.7;
vector-effect: non-scaling-stroke;
}
.wave-tag {
flex-shrink: 0;
font-family: var(--deepdrft-font-mono);
font-size: 0.62rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--deepdrft-navy);
white-space: nowrap; white-space: nowrap;
} }
/* ── SECTION (from Home.razor.css) ── */ /* ── Movement intro — prose hanging at a consistent left edge ── */
.section { .movement-intro {
padding: 7rem 3rem; max-width: 60ch;
background: var(--deepdrft-white); margin-bottom: 5rem;
} }
@media (min-width: 960px) { .movement-label {
.section-header-grid { font-family: var(--deepdrft-font-mono);
align-items: end; font-size: 0.62rem;
} letter-spacing: 0.28em;
color: var(--deepdrft-green-accent);
text-transform: uppercase;
margin-bottom: 1.4rem;
} }
.section-label { .movement-title {
font-family: var(--deepdrft-font-display);
font-size: clamp(2.6rem, 5vw, 4.2rem);
font-weight: 300;
line-height: 1.02;
color: var(--deepdrft-navy);
margin-bottom: 2rem;
}
.movement-title em {
font-style: italic;
color: var(--deepdrft-green);
}
.movement-prose {
font-family: var(--deepdrft-font-body);
font-size: 0.95rem;
line-height: 1.85;
color: var(--deepdrft-navy);
opacity: 0.72;
max-width: 56ch;
}
/* ── Member bio pair — framed portrait insets with rail-side captions ──
Assembled from the existing type tokens (display serif name, mono caption/role,
body prose). The cards are offset/staggered rather than an even grid. */
.bio-pair {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 3rem;
align-items: start;
}
/* Stagger the second card downward so the pair reads as editorial layout, not a
symmetric grid. */
.bio-card:nth-child(2) {
margin-top: 4rem;
}
.bio-card {
display: flex;
flex-direction: column;
}
.bio-portrait {
width: 100%;
overflow: hidden;
border: 1px solid var(--deepdrft-border);
}
/* Graceful-degrade slot shown until a portrait file lands. A flat tonal panel in
the navy family with the site's portrait aspect precedent (1365×2048). */
.bio-portrait-placeholder {
width: 100%;
aspect-ratio: 1365 / 2048;
max-height: 56vh;
background:
linear-gradient(160deg,
color-mix(in srgb, var(--deepdrft-navy) 8%, var(--deepdrft-white)) 0%,
color-mix(in srgb, var(--deepdrft-navy) 16%, var(--deepdrft-white)) 100%);
}
/* The marginalia caption — mono, sits directly under the framed portrait. */
.bio-caption {
font-family: var(--deepdrft-font-mono);
font-size: 0.56rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--deepdrft-muted);
margin-top: 0.9rem;
padding-left: 0.1rem;
}
.bio-meta {
padding-top: 1.4rem;
}
.bio-name {
font-family: var(--deepdrft-font-display);
font-size: 2rem;
font-weight: 300;
line-height: 1.1;
color: var(--deepdrft-navy);
margin-bottom: 1rem;
}
.bio-body {
font-family: var(--deepdrft-font-body);
font-size: 0.85rem;
line-height: 1.8;
color: var(--deepdrft-navy);
opacity: 0.7;
}
/* ── Inset framed figure (gear shot) with rail-side caption ── */
.movement-figure {
margin: 5rem 0 0;
}
.movement-figure ::deep .parallax-window {
border: 1px solid var(--deepdrft-border);
}
.figure-caption {
font-family: var(--deepdrft-font-mono);
font-size: 0.56rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--deepdrft-muted);
margin-top: 0.9rem;
}
/* ══════════════════ THE PROCESS — dark band (reused vocabulary) ══════════════════ */
.process-band {
background: var(--deepdrft-navy);
padding: 4.5rem 3rem;
color: var(--deepdrft-white);
}
.process-label {
font-family: var(--deepdrft-font-mono); font-family: var(--deepdrft-font-mono);
font-size: 0.62rem; font-size: 0.62rem;
letter-spacing: 0.28em; letter-spacing: 0.28em;
@@ -141,167 +354,20 @@
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
} }
.section-title { .process-title {
font-family: var(--deepdrft-font-display); font-family: var(--deepdrft-font-display);
font-size: clamp(2.8rem, 5vw, 4.5rem); font-size: clamp(2.4rem, 4.5vw, 3.8rem);
font-weight: 300; font-weight: 300;
line-height: 1; line-height: 1.02;
color: var(--deepdrft-navy);
}
.section-title em {
font-style: italic;
color: var(--deepdrft-green);
}
/* ::deep required: the class is on the MudItem-rendered div, which does not carry
this component's scope attribute. */
.section-header-grid ::deep .section-body-item {
display: flex;
align-items: center;
}
.section-body {
display: flex;
max-width: 560px;
margin: auto;
align-content: center;
justify-content: center;
}
.section-body p {
display: flex;
font-family: var(--deepdrft-font-body);
font-size: 0.9rem;
line-height: 1.8;
color: var(--deepdrft-navy);
opacity: 0.65;
max-width: 52ch;
}
/* ── MEDIUM GRID (from Home.razor.css) ── */
.medium-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 56px;
margin-bottom: 4rem;
}
@media (max-width: 959px) {
.medium-grid { grid-template-columns: repeat(2, 1fr); }
.medium-card:last-child { grid-column: 1 / -1; }
}
@media (max-width: 599px) {
.medium-grid { grid-template-columns: 1fr; }
.medium-card:last-child { grid-column: auto; }
}
.medium-card {
background: var(--deepdrft-white);
border: 1px solid var(--deepdrft-border);
cursor: pointer;
overflow: hidden;
text-decoration: none;
display: block;
position: relative;
max-width: 380px;
margin: 0 auto;
}
.medium-card::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--deepdrft-green-accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s;
z-index: 1;
}
.medium-card:hover::after { transform: scaleX(1); }
.medium-image {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
background-size: cover;
background-position: center;
transition: transform 0.5s ease;
}
.medium-card:hover .medium-image { transform: scale(1.05); }
.medium-scrim {
position: absolute;
inset: 0;
background: linear-gradient(to bottom,
rgba(17, 35, 56, 0.0) 40%,
rgba(17, 35, 56, 0.35) 100%);
transition: opacity 0.3s;
opacity: 0.7;
}
.medium-card:hover .medium-scrim { opacity: 1; }
.medium-body {
padding: 2rem 1.5rem;
position: relative;
}
.medium-type {
font-family: var(--deepdrft-font-mono);
font-size: 0.58rem;
letter-spacing: 0.2em;
color: var(--deepdrft-muted);
text-transform: uppercase;
margin-bottom: 0.6rem;
}
.medium-name {
font-family: var(--deepdrft-font-display);
font-size: 1.6rem;
font-weight: 400;
color: var(--deepdrft-navy);
margin-bottom: 0.75rem;
line-height: 1.1;
}
.medium-desc {
font-family: var(--deepdrft-font-body);
font-size: 0.82rem;
line-height: 1.65;
color: var(--deepdrft-navy);
opacity: 0.6;
}
/* ── DARK FEATURE SECTION (from Home.razor.css) ── */
.section-dark {
background: var(--deepdrft-navy);
padding: 7rem 3rem;
color: var(--deepdrft-white); color: var(--deepdrft-white);
} }
.section-label-dark { .process-title em {
color: var(--deepdrft-green-accent);
}
.section-title-dark {
color: var(--deepdrft-white);
margin-top: 0.5rem;
}
.section-title-dark em {
font-style: italic; font-style: italic;
color: var(--deepdrft-green-accent); color: var(--deepdrft-green-accent);
} }
/* Process standfirst (COPY D-intro) — sits between the dark title and the gear cards. */ .process-standfirst {
.section-dark-standfirst {
font-family: var(--deepdrft-font-body); font-family: var(--deepdrft-font-body);
font-size: 0.92rem; font-size: 0.92rem;
line-height: 1.8; line-height: 1.8;
@@ -312,35 +378,26 @@
.features-grid { .features-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 0; gap: 0;
border: 1px solid rgba(250, 250, 248, 0.08); border: 1px solid rgba(250, 250, 248, 0.08);
margin-top: 4rem; margin-top: 3.5rem;
} }
.feature-card { .feature-card {
padding: 2.5rem; padding: 2.5rem;
border-right: 1px solid rgba(250, 250, 248, 0.08); border-right: 1px solid rgba(250, 250, 248, 0.08);
border-bottom: 1px solid rgba(250, 250, 248, 0.08);
transition: background 0.3s; transition: background 0.3s;
} }
.feature-card:last-child { border-right: none; } /* 2×2 grid: kill the right border on the right column and the bottom border on the
last row so the outer frame stays clean. */
.feature-card:nth-child(2n) { border-right: none; }
.feature-card:nth-child(n + 3) { border-bottom: none; }
.feature-card:hover { background: rgba(250, 250, 248, 0.04); } .feature-card:hover { background: rgba(250, 250, 248, 0.04); }
@media (max-width: 960px) {
.features-grid { grid-template-columns: repeat(2, 1fr); }
.feature-card { border-right: none; border-bottom: 1px solid rgba(250, 250, 248, 0.08); }
.feature-card:last-child { border-bottom: none; }
.section-dark-standfirst { font-size: 0.88rem; }
}
@media (max-width: 599px) {
.features-grid { grid-template-columns: 1fr; }
.feature-card { border-right: none; border-bottom: 1px solid rgba(250, 250, 248, 0.08); }
.feature-card:last-child { border-bottom: none; }
}
.feature-icon { .feature-icon {
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
@@ -375,83 +432,76 @@
color: rgba(250, 250, 248, 0.45); color: rgba(250, 250, 248, 0.45);
} }
/* ── SPLIT (from Home.razor.css) ── */ /* ══════════════════ THE PRODUCT — medium list + pull-quote ══════════════════
.section-split { A stacked editorial definition list, not Home's card grid. */
min-height: 60vh; .medium-list {
list-style: none;
margin: 0 0 5rem;
padding: 0;
border-top: 1px solid var(--deepdrft-border);
max-width: 60ch;
} }
@media (max-width: 960px) { .medium-row {
.section-split { min-height: auto; } border-bottom: 1px solid var(--deepdrft-border);
} }
.split-left { .medium-row a {
background: var(--deepdrft-green);
padding: 6rem 4rem;
display: flex; display: flex;
flex-direction: column; align-items: baseline;
justify-content: center; gap: 1.5rem;
position: relative; padding: 1.6rem 0.4rem;
overflow: hidden; text-decoration: none;
height: 100%; transition: padding-left 0.25s ease;
} }
.split-left::before { .medium-row a:hover { padding-left: 1.2rem; }
content: '';
position: absolute;
top: -100px;
right: -100px;
width: 400px;
height: 400px;
border-radius: 50%;
background: rgba(61, 122, 104, 0.3);
}
.split-right { .medium-row-name {
display: flex; flex-shrink: 0;
flex-direction: column;
justify-content: center;
background: var(--deepdrft-white);
height: 100%;
}
.split-eyebrow {
font-family: var(--deepdrft-font-mono);
font-size: 0.62rem;
letter-spacing: 0.28em;
color: rgba(250, 250, 248, 0.6);
text-transform: uppercase;
margin-bottom: 1.5rem;
position: relative;
z-index: 1;
}
.split-title {
font-family: var(--deepdrft-font-display); font-family: var(--deepdrft-font-display);
font-size: clamp(2.5rem, 4vw, 3.8rem); font-size: 1.5rem;
font-weight: 300; font-weight: 400;
color: var(--deepdrft-white); color: var(--deepdrft-navy);
line-height: 1.05; min-width: 7rem;
margin-bottom: 1.5rem;
position: relative;
z-index: 1;
} }
.split-title em { .medium-row a:hover .medium-row-name { color: var(--deepdrft-green-accent); }
font-style: italic;
opacity: 0.65;
}
.split-body { .medium-row-desc {
font-family: var(--deepdrft-font-body); font-family: var(--deepdrft-font-body);
font-size: 0.88rem; font-size: 0.85rem;
line-height: 1.8; line-height: 1.6;
color: rgba(250, 250, 248, 0.6); color: var(--deepdrft-navy);
max-width: 42ch; opacity: 0.6;
position: relative;
z-index: 1;
} }
/* ── CTA BANNER (from Home.razor.css) ── */ /* The sharp pull-quote — breaks LEFT into the rail margin at large serif scale. */
.pull-quote {
margin: 0;
margin-left: -7rem;
max-width: 22ch;
}
.pull-eyebrow {
display: block;
font-family: var(--deepdrft-font-mono);
font-size: 0.6rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--deepdrft-green-accent);
margin-bottom: 1.4rem;
}
.pull-quote p {
font-family: var(--deepdrft-font-display);
font-size: clamp(1.8rem, 3.4vw, 2.9rem);
font-weight: 300;
line-height: 1.15;
color: var(--deepdrft-navy);
}
/* ══════════════════ CLOSING CTA (reused vocabulary) ══════════════════ */
.cta-banner { .cta-banner {
background: var(--deepdrft-navy); background: var(--deepdrft-navy);
padding: 6rem 3rem; padding: 6rem 3rem;
@@ -552,7 +602,83 @@
.btn-outline-white:hover { border-color: var(--deepdrft-white); } .btn-outline-white:hover { border-color: var(--deepdrft-white); }
/* ══════════════════ RESPONSIVE COLLAPSE ══════════════════
Below 960px the rail collapses: the spine + vertical numeral can't survive a
narrow viewport, so the numeral goes inline above each movement (horizontal,
left-aligned) and the marginalia folds away. Content goes single-column. */
@media (max-width: 960px) {
.movement {
display: block;
}
.rail {
padding: 2.5rem 1.5rem 0;
}
/* Spine becomes a short horizontal accent under the inline numeral. */
.rail-line {
position: static;
width: 3rem;
height: 2px;
margin-top: 1rem;
background: var(--deepdrft-border);
}
.rail-numeral {
position: static;
opacity: 0.18;
padding-left: 0;
font-size: clamp(3.5rem, 16vw, 5.5rem);
}
.movement.is-active .rail-numeral {
opacity: 0.95;
}
/* Marginalia is editorial chrome the narrow column can't host — drop it. */
.rail-margin {
display: none;
}
.movement-content {
padding: 2.5rem 1.5rem 3.5rem;
}
.process-band { padding: 3.5rem 1.5rem; }
/* Pull-quote can't break into a rail that no longer exists. */
.pull-quote {
margin-left: 0;
max-width: 100%;
}
}
@media (max-width: 720px) {
/* Bio pair stacks; drop the stagger so cards align cleanly. */
.bio-pair {
grid-template-columns: 1fr;
gap: 3.5rem;
}
.bio-card:nth-child(2) {
margin-top: 0;
}
}
@media (max-width: 599px) { @media (max-width: 599px) {
.features-grid { grid-template-columns: 1fr; }
.feature-card {
border-right: none;
border-bottom: 1px solid rgba(250, 250, 248, 0.08);
}
.feature-card:last-child { border-bottom: none; }
.medium-row a {
flex-direction: column;
gap: 0.4rem;
}
.cta-banner { .cta-banner {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -576,63 +702,3 @@
right: -0.5rem; right: -0.5rem;
} }
} }
/* ── BIO CARDS (new — the only bespoke styling on About) ──
Assembled from the existing type tokens (display serif for the name, mono for
the role line, body for the bio), so they sit inside the established vocabulary
rather than inventing a new one. */
.bio-card {
height: 100%;
background: var(--deepdrft-white);
border: 1px solid var(--deepdrft-border);
display: flex;
flex-direction: column;
}
.bio-portrait {
width: 100%;
overflow: hidden;
}
/* Graceful-degrade slot shown until a portrait file lands. A flat tonal panel in
the navy family with the site's aspect precedent (1365×2048 portrait). */
.bio-portrait-placeholder {
width: 100%;
aspect-ratio: 1365 / 2048;
max-height: 60vh;
background:
linear-gradient(160deg,
color-mix(in srgb, var(--deepdrft-navy) 8%, var(--deepdrft-white)) 0%,
color-mix(in srgb, var(--deepdrft-navy) 16%, var(--deepdrft-white)) 100%);
border-bottom: 1px solid var(--deepdrft-border);
}
.bio-meta {
padding: 2.25rem 2rem 2.5rem;
}
.bio-name {
font-family: var(--deepdrft-font-display);
font-size: 2rem;
font-weight: 300;
line-height: 1.1;
color: var(--deepdrft-navy);
margin-bottom: 0.6rem;
}
.bio-role {
font-family: var(--deepdrft-font-mono);
font-size: 0.6rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--deepdrft-green-accent);
margin-bottom: 1.4rem;
}
.bio-body {
font-family: var(--deepdrft-font-body);
font-size: 0.85rem;
line-height: 1.8;
color: var(--deepdrft-navy);
opacity: 0.7;
}
@@ -0,0 +1,65 @@
/**
* About-page rail active-numeral highlight.
*
* The Liner Notes layout carries one oversized Bodoni numeral per movement in a
* persistent left rail. This module lights the numeral of whichever movement is
* currently in view by toggling `.is-active` on the movement element; the CSS does
* the colour transition. Pure progressive enhancement — the numerals render
* statically in low-opacity navy without this, so a load failure or a no-JS client
* degrades to a still-legible page.
*
* One observer at a time, re-pointed on each `observe` call. The active movement is
* the one nearest the top of the viewport among those currently intersecting, which
* keeps a single numeral lit during the scroll rather than flickering between
* adjacent movements at the boundary.
*/
let observer: IntersectionObserver | null = null;
let observed: Element[] = [];
function refreshActive(): void {
let best: Element | null = null;
let bestTop = Number.POSITIVE_INFINITY;
for (const el of observed) {
const rect = el.getBoundingClientRect();
const viewportH = window.innerHeight || document.documentElement.clientHeight;
// In view at all (any overlap with the viewport).
const inView = rect.bottom > 0 && rect.top < viewportH;
if (!inView) continue;
// Prefer the movement whose top is closest to (but not far below) the fold.
const distance = Math.abs(rect.top);
if (distance < bestTop) {
bestTop = distance;
best = el;
}
}
for (const el of observed) {
el.classList.toggle('is-active', el === best);
}
}
export function observe(...elements: Element[]): void {
unobserve();
observed = elements.filter(Boolean);
if (observed.length === 0) return;
// IntersectionObserver only tells us *that* visibility changed; the actual
// "which is nearest the fold" decision is recomputed from live rects so the
// choice stays correct mid-scroll.
observer = new IntersectionObserver(() => refreshActive(), {
threshold: [0, 0.25, 0.5, 0.75, 1],
});
for (const el of observed) observer.observe(el);
// Seed once so the first movement lights before any scroll.
refreshActive();
}
export function unobserve(): void {
observer?.disconnect();
observer = null;
for (const el of observed) el.classList.remove('is-active');
observed = [];
}