Merge p12-w2-about-liner-notes into dev (About page Liner Notes editorial redesign)
This commit is contained in:
@@ -1,9 +1,27 @@
|
||||
@page "/about"
|
||||
@using DeepDrftPublic.Client.Controls
|
||||
@implements IAsyncDisposable
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<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). ── *@
|
||||
<section class="hero">
|
||||
<MudGrid Spacing="0" Style="height: 100%;">
|
||||
@@ -18,7 +36,7 @@
|
||||
</MudItem>
|
||||
|
||||
<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">
|
||||
<ParallaxImage Image1="img/dd-duo-2-bw.jpg"
|
||||
Image2="img/dd-duo-2.jpeg"
|
||||
@@ -32,55 +50,43 @@
|
||||
</div>
|
||||
</MudItem>
|
||||
</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>
|
||||
|
||||
@* ════════════════════ MOVEMENT ONE — THE PEOPLE (pathos) ════════════════════ *@
|
||||
<div class="section-divider">
|
||||
<div class="divider-line"></div>
|
||||
<div class="divider-tag">The People</div>
|
||||
<div class="divider-line"></div>
|
||||
</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 — 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>
|
||||
@* ════════════════ MOVEMENT ONE — THE PEOPLE (pathos) ════════════════ *@
|
||||
<div class="movement" data-movement="1" @ref="_movementOne">
|
||||
<div class="rail" aria-hidden="true">
|
||||
<div class="rail-line"></div>
|
||||
<div class="rail-numeral">01</div>
|
||||
<div class="rail-margin">Charleston, SC</div>
|
||||
</div>
|
||||
|
||||
@* Member bio pair — two cards side by side; each composes with the body absent
|
||||
(Khabran ships with an empty body slot, same null-renders-nothing discipline
|
||||
as ReleaseDescription). *@
|
||||
<MudGrid>
|
||||
@foreach (var member in _members)
|
||||
{
|
||||
<MudItem xs="12" md="6">
|
||||
<div class="bio-card">
|
||||
<div class="movement-content">
|
||||
@* Waveform movement divider — a static SVG oscillation stroke carrying the
|
||||
movement tag. The folded-in D3 signature motif. *@
|
||||
<div class="wave-divider">
|
||||
<svg class="wave-stroke" viewBox="0 0 1200 40" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="@WavePath" />
|
||||
</svg>
|
||||
<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 — 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">
|
||||
@if (member.PortraitImage1 is not null)
|
||||
{
|
||||
@@ -100,170 +106,150 @@
|
||||
<div class="bio-portrait-placeholder" aria-hidden="true"></div>
|
||||
}
|
||||
</div>
|
||||
<div class="bio-caption">@member.Name · @member.Role</div>
|
||||
<div class="bio-meta">
|
||||
<div class="bio-name">@member.Name</div>
|
||||
<div class="bio-role">@member.Role</div>
|
||||
@if (!string.IsNullOrWhiteSpace(member.Bio))
|
||||
{
|
||||
<p class="bio-body">@member.Bio</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</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>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Dark feature band — gear-stage cards. The dark ground carries the analytical register. *@
|
||||
<section class="section-dark">
|
||||
<div class="section-label section-label-dark">How It's Made</div>
|
||||
<h2 class="section-title section-title-dark">
|
||||
Digital, Analog,<br /><em>Whatever Moves</em>
|
||||
</h2>
|
||||
|
||||
<p class="section-dark-standfirst">
|
||||
It doesn't matter how — digital or analog, hard or soft, bought or built — 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 — 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 — sequenced, twisted, and pushed in the moment. Built for the room, the warehouse, the night that doesn't repeat.</div>
|
||||
</div>
|
||||
@* ════════════════ MOVEMENT TWO — THE PROCESS (logos) ════════════════ *@
|
||||
<div class="movement" data-movement="2" @ref="_movementTwo">
|
||||
<div class="rail" aria-hidden="true">
|
||||
<div class="rail-line"></div>
|
||||
<div class="rail-numeral">02</div>
|
||||
<div class="rail-margin">the live rig</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@* IMG SLOT D — hands-on-gear band, the literal proof-of-effort image. *@
|
||||
<section>
|
||||
<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" />
|
||||
</section>
|
||||
<div class="movement-content">
|
||||
<div class="wave-divider">
|
||||
<svg class="wave-stroke" viewBox="0 0 1200 40" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="@WavePath" />
|
||||
</svg>
|
||||
<span class="wave-tag">The Process</span>
|
||||
</div>
|
||||
|
||||
@* ════════════════════ MOVEMENT THREE — THE PRODUCT (ethos) ════════════════════ *@
|
||||
<div class="section-divider">
|
||||
<div class="divider-line"></div>
|
||||
<div class="divider-tag">The Product</div>
|
||||
<div class="divider-line"></div>
|
||||
@* Dark band — gear-stage cards. The navy ground carries the analytical register. *@
|
||||
<div class="process-band">
|
||||
<div class="process-label">How It's Made</div>
|
||||
<h2 class="process-title">Digital, Analog, <em>Whatever Moves</em></h2>
|
||||
|
||||
<p class="process-standfirst">
|
||||
It doesn't matter how — digital or analog, hard or soft, bought or built — 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 — 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 — 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>
|
||||
|
||||
@* Product intro — two-column section framing the catalogue as evidence. *@
|
||||
<section class="section">
|
||||
<div class="section-header-grid">
|
||||
<MudGrid Style="margin-bottom: 5rem;">
|
||||
<MudItem xs="12" md="4">
|
||||
<div class="section-label">The Output</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>
|
||||
@* ════════════════ MOVEMENT THREE — THE PRODUCT (ethos) ════════════════ *@
|
||||
<div class="movement" data-movement="3" @ref="_movementThree">
|
||||
<div class="rail" aria-hidden="true">
|
||||
<div class="rail-line"></div>
|
||||
<div class="rail-numeral">03</div>
|
||||
<div class="rail-margin">in the swamp</div>
|
||||
</div>
|
||||
|
||||
@* Medium triptych — one-line frame of each medium; definitions, not a re-pitch. *@
|
||||
<div class="medium-grid">
|
||||
<a class="medium-card" href="/cuts">
|
||||
<div class="medium-image" style="background-image: url('img/dd-studio.jpg');">
|
||||
<div class="medium-scrim"></div>
|
||||
</div>
|
||||
<div class="medium-body">
|
||||
<div class="medium-type">Studio</div>
|
||||
<div class="medium-name">Cuts</div>
|
||||
<div class="medium-desc">Studio work, composed and finished.</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="medium-card" href="/sessions">
|
||||
<div class="medium-image" style="background-image: url('img/dd-live.jpeg');">
|
||||
<div class="medium-scrim"></div>
|
||||
</div>
|
||||
<div class="medium-body">
|
||||
<div class="medium-type">Live</div>
|
||||
<div class="medium-name">Sessions</div>
|
||||
<div class="medium-desc">Live, caught once, never the same twice.</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="medium-card" href="/mixes">
|
||||
<div class="medium-image" style="background-image: url('img/dd-dj.jpeg');">
|
||||
<div class="medium-scrim"></div>
|
||||
</div>
|
||||
<div class="medium-body">
|
||||
<div class="medium-type">DJ Set</div>
|
||||
<div class="medium-name">Mixes</div>
|
||||
<div class="medium-desc">Uninterrupted sets, start to finish.</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="movement-content">
|
||||
<div class="wave-divider">
|
||||
<svg class="wave-stroke" viewBox="0 0 1200 40" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="@WavePath" />
|
||||
</svg>
|
||||
<span class="wave-tag">The Product</span>
|
||||
</div>
|
||||
|
||||
<div class="movement-intro">
|
||||
<div class="movement-label">The Output</div>
|
||||
<h2 class="movement-title">Classics, with a <em>Twist</em></h2>
|
||||
<p class="movement-prose">
|
||||
Everything ends up here, in the catalogue. It's proof people in Charleston are pushing the sound of the club.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@* Medium triptych — one-line frame of each medium; definitions, not a re-pitch.
|
||||
A stacked editorial list rather than Home's card grid. *@
|
||||
<ul class="medium-list">
|
||||
<li class="medium-row">
|
||||
<a href="/cuts">
|
||||
<span class="medium-row-name">Cuts</span>
|
||||
<span class="medium-row-desc">Studio work, composed and finished.</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="medium-row">
|
||||
<a href="/sessions">
|
||||
<span class="medium-row-name">Sessions</span>
|
||||
<span class="medium-row-desc">Live, caught once, never the same twice.</span>
|
||||
</a>
|
||||
</li>
|
||||
<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 — on the street, in the swamp, with a PA, a generator, and a bunch of good vibes.
|
||||
</p>
|
||||
</blockquote>
|
||||
</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 — 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>
|
||||
|
||||
@* ── Closing CTA into the catalogue ── *@
|
||||
@@ -278,23 +264,78 @@
|
||||
</div>
|
||||
</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 {
|
||||
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
|
||||
// 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").
|
||||
|
||||
@@ -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
|
||||
primitives (.hero-*, .section, .section-divider, .section-dark, .section-split,
|
||||
.medium-*, .cta-*, .feature-*) live SCOPED in Home.razor.css and DeepDrftHero.razor.css
|
||||
— they are not in the global stylesheet, so Blazor CSS isolation will not share them
|
||||
with this component. The primitives are therefore re-declared here, verbatim from those
|
||||
sources, so About renders identically without touching Home's rendering surface. The only
|
||||
genuinely new styling is the two bio-card pieces (.bio-*) at the end. */
|
||||
This page diverges from Home by composition, not vocabulary. The backbone is a
|
||||
persistent left RAIL (a continuous vertical hairline carrying oversized Bodoni
|
||||
movement numerals + mono marginalia) with the content column offset asymmetrically
|
||||
to its right. Movement boundaries are rendered as a hand-authored SVG waveform
|
||||
stroke (the D3 motif folded in). Palette tokens, type stack, the dark Process band,
|
||||
the feature-card grid, the CTA, and the bw↔colour ParallaxImage crossfade are all
|
||||
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) ── */
|
||||
@keyframes fade-up {
|
||||
@@ -19,7 +23,7 @@
|
||||
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 {
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
@@ -96,43 +100,252 @@
|
||||
animation-delay: 0.44s;
|
||||
}
|
||||
|
||||
/* ── DIVIDER (from Home.razor.css) ── */
|
||||
.section-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
padding: 2rem 3rem;
|
||||
/* ══════════════════ THE RAIL + SPINE — the signature device ══════════════════
|
||||
|
||||
Each movement is a two-column grid: a narrow rail column on the left carrying the
|
||||
continuous vertical hairline (the narrative spine), the oversized Bodoni numeral,
|
||||
and the mono marginalia; the content column to its right. The rail column is
|
||||
~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);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
.rail {
|
||||
position: relative;
|
||||
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);
|
||||
}
|
||||
|
||||
.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-size: 0.6rem;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--deepdrft-muted);
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.24em;
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── SECTION (from Home.razor.css) ── */
|
||||
.section {
|
||||
padding: 7rem 3rem;
|
||||
background: var(--deepdrft-white);
|
||||
/* ── Movement intro — prose hanging at a consistent left edge ── */
|
||||
.movement-intro {
|
||||
max-width: 60ch;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.section-header-grid {
|
||||
align-items: end;
|
||||
}
|
||||
.movement-label {
|
||||
font-family: var(--deepdrft-font-mono);
|
||||
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-size: 0.62rem;
|
||||
letter-spacing: 0.28em;
|
||||
@@ -141,167 +354,20 @@
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
.process-title {
|
||||
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;
|
||||
line-height: 1;
|
||||
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;
|
||||
line-height: 1.02;
|
||||
color: var(--deepdrft-white);
|
||||
}
|
||||
|
||||
.section-label-dark {
|
||||
color: var(--deepdrft-green-accent);
|
||||
}
|
||||
|
||||
.section-title-dark {
|
||||
color: var(--deepdrft-white);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title-dark em {
|
||||
.process-title em {
|
||||
font-style: italic;
|
||||
color: var(--deepdrft-green-accent);
|
||||
}
|
||||
|
||||
/* Process standfirst (COPY D-intro) — sits between the dark title and the gear cards. */
|
||||
.section-dark-standfirst {
|
||||
.process-standfirst {
|
||||
font-family: var(--deepdrft-font-body);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.8;
|
||||
@@ -312,35 +378,26 @@
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0;
|
||||
border: 1px solid rgba(250, 250, 248, 0.08);
|
||||
margin-top: 4rem;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 2.5rem;
|
||||
border-right: 1px solid rgba(250, 250, 248, 0.08);
|
||||
border-bottom: 1px solid rgba(250, 250, 248, 0.08);
|
||||
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); }
|
||||
|
||||
@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 {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
@@ -375,83 +432,76 @@
|
||||
color: rgba(250, 250, 248, 0.45);
|
||||
}
|
||||
|
||||
/* ── SPLIT (from Home.razor.css) ── */
|
||||
.section-split {
|
||||
min-height: 60vh;
|
||||
/* ══════════════════ THE PRODUCT — medium list + pull-quote ══════════════════
|
||||
A stacked editorial definition list, not Home's card grid. */
|
||||
.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) {
|
||||
.section-split { min-height: auto; }
|
||||
.medium-row {
|
||||
border-bottom: 1px solid var(--deepdrft-border);
|
||||
}
|
||||
|
||||
.split-left {
|
||||
background: var(--deepdrft-green);
|
||||
padding: 6rem 4rem;
|
||||
.medium-row a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
align-items: baseline;
|
||||
gap: 1.5rem;
|
||||
padding: 1.6rem 0.4rem;
|
||||
text-decoration: none;
|
||||
transition: padding-left 0.25s ease;
|
||||
}
|
||||
|
||||
.split-left::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 50%;
|
||||
background: rgba(61, 122, 104, 0.3);
|
||||
}
|
||||
.medium-row a:hover { padding-left: 1.2rem; }
|
||||
|
||||
.split-right {
|
||||
display: flex;
|
||||
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 {
|
||||
.medium-row-name {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--deepdrft-font-display);
|
||||
font-size: clamp(2.5rem, 4vw, 3.8rem);
|
||||
font-weight: 300;
|
||||
color: var(--deepdrft-white);
|
||||
line-height: 1.05;
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: var(--deepdrft-navy);
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
.split-title em {
|
||||
font-style: italic;
|
||||
opacity: 0.65;
|
||||
}
|
||||
.medium-row a:hover .medium-row-name { color: var(--deepdrft-green-accent); }
|
||||
|
||||
.split-body {
|
||||
.medium-row-desc {
|
||||
font-family: var(--deepdrft-font-body);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.8;
|
||||
color: rgba(250, 250, 248, 0.6);
|
||||
max-width: 42ch;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
color: var(--deepdrft-navy);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
background: var(--deepdrft-navy);
|
||||
padding: 6rem 3rem;
|
||||
@@ -552,7 +602,83 @@
|
||||
|
||||
.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) {
|
||||
.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 {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -576,63 +702,3 @@
|
||||
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 = [];
|
||||
}
|
||||
Reference in New Issue
Block a user