feat: chapter openers over the road, reading panels, next-exit signs, two-column case studies and a destination ending

This commit is contained in:
2026-10-10 18:57:03 +03:00
parent abae50c22d
commit e2f2ed6b9b
6 changed files with 371 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import './styles/chromatic.css';
import './styles/refinements.css'; import './styles/refinements.css';
import './styles/title-navigation.css'; import './styles/title-navigation.css';
import './styles/wayfinding.css'; import './styles/wayfinding.css';
import './styles/route.css';
import './styles/print.css'; import './styles/print.css';
defineProps<{ inputs: Readonly<ProductionInputs> }>(); defineProps<{ inputs: Readonly<ProductionInputs> }>();

View File

@@ -10,11 +10,13 @@ defineProps<{
exit?: string | undefined; exit?: string | undefined;
/** Next chapter label for the plate's "then" line (decorative, CSS-generated). */ /** Next chapter label for the plate's "then" line (decorative, CSS-generated). */
nextLabel?: string | undefined; nextLabel?: string | undefined;
/** Next chapter's exit number, for the end-of-chapter "next exit" sign. */
nextExit?: string | undefined;
}>(); }>();
</script> </script>
<template> <template>
<section :id="id" class="portfolio-section" :aria-labelledby="`${id}-heading`" :data-layout="layout" :data-exit="exit" :data-next="nextLabel"> <section :id="id" class="portfolio-section" :aria-labelledby="`${id}-heading`" :data-layout="layout" :data-exit="exit" :data-next="nextLabel" :data-next-exit="nextExit">
<div v-if="$slots.lead" class="project-stage__lead"> <div v-if="$slots.lead" class="project-stage__lead">
<p v-if="eyebrow" class="section-eyebrow">{{ eyebrow }}</p> <p v-if="eyebrow" class="section-eyebrow">{{ eyebrow }}</p>
<h2 :id="`${id}-heading`" tabindex="-1">{{ heading }}</h2> <h2 :id="`${id}-heading`" tabindex="-1">{{ heading }}</h2>
@@ -24,6 +26,7 @@ defineProps<{
<p v-if="eyebrow" class="section-eyebrow">{{ eyebrow }}</p> <p v-if="eyebrow" class="section-eyebrow">{{ eyebrow }}</p>
<h2 :id="`${id}-heading`" tabindex="-1">{{ heading }}</h2> <h2 :id="`${id}-heading`" tabindex="-1">{{ heading }}</h2>
</template> </template>
<slot /> <slot v-if="$slots.lead" />
<div v-else class="chapter-body"><slot /></div>
</section> </section>
</template> </template>

View File

@@ -98,6 +98,7 @@
} }
.portfolio-section > p, .portfolio-section > p,
.portfolio-section > .chapter-body > p,
.portfolio-section article p, .portfolio-section article p,
.portfolio-section dd, .portfolio-section dd,
.portfolio-section li { .portfolio-section li {
@@ -105,6 +106,7 @@
} }
.portfolio-section > * + *, .portfolio-section > * + *,
.portfolio-section > .chapter-body > * + *,
.portfolio-section article > * + *, .portfolio-section article > * + *,
.portfolio-section li + li { .portfolio-section li + li {
margin-block-start: var(--space-2); margin-block-start: var(--space-2);

View File

@@ -166,7 +166,7 @@ body {
font-size: 0.7rem; font-size: 0.7rem;
letter-spacing: 0.1em; letter-spacing: 0.1em;
} }
.portfolio-section[data-layout='right-rail'] > p { .portfolio-section[data-layout='right-rail'] > .chapter-body > p {
font-size: clamp(19px, 1.5vw, 24px); font-size: clamp(19px, 1.5vw, 24px);
line-height: 1.6; line-height: 1.6;
letter-spacing: -0.02em; letter-spacing: -0.02em;
@@ -635,7 +635,7 @@ body {
.portfolio-section[data-layout='project-stage'] > .section-eyebrow { .portfolio-section[data-layout='project-stage'] > .section-eyebrow {
margin-inline: 12px; margin-inline: 12px;
} }
.portfolio-section[data-layout='right-rail'] > p { .portfolio-section[data-layout='right-rail'] > .chapter-body > p {
font-size: 20px; font-size: 20px;
} }
.experience-record { .experience-record {

360
src/styles/route.css Normal file
View File

@@ -0,0 +1,360 @@
/* The Route (round 3). Chapters stop being identical glass cards over the
world. Each one opens with its exit tab and a large heading standing over
the road, then a reading panel, then a "next exit" sign; between chapters
the page leaves a stretch of open road so the roadside districts show.
Navigation becomes a route map. Every added label is CSS generated content
or an aria-label, so the semantic document is unchanged. Imported after
wayfinding.css, before print.css. */
/* ---------- Chapter openers over the road (tablet and up) ---------- */
@media screen and (min-width: 768px) {
.reading-stage .portfolio-section {
margin-block: clamp(220px, 44vh, 480px) 0;
padding-block-end: clamp(150px, 20vh, 230px);
}
.reading-stage .portfolio-section:not([data-layout='project-stage']) {
padding-inline: 0;
padding-block-start: 3rem;
border: 0;
border-radius: 0;
background: none;
box-shadow: none;
}
.reading-stage .portfolio-section[data-current] {
animation: none;
}
/* The exit tab now hangs above the heading, not on a panel edge. */
.reading-stage .portfolio-section:not([data-layout='project-stage'])[data-exit]::before {
inset-inline-start: 0;
translate: none;
padding: 0.55rem 0.85rem 0.5rem;
font-size: 0.78rem;
}
.reading-stage :is(.portfolio-section, .project-stage__lead) > h2 {
inline-size: fit-content;
max-inline-size: 100%;
margin-block: 0 clamp(26px, 4vh, 44px);
color: var(--text);
font-size: clamp(54px, 7.2vw, 124px);
font-weight: 620;
line-height: 0.94;
letter-spacing: -0.058em;
text-wrap: balance;
/* A dark halo instead of a plate: the heading stands in the scene but
keeps its contrast over the sun and the neon horizon. */
text-shadow:
0 0 2px rgb(9 4 22 / 95%),
0 0.02em 0.16em rgb(9 4 22 / 92%),
0 0.06em 0.6em rgb(9 4 22 / 78%);
}
.reading-stage :is(.portfolio-section, .project-stage__lead) > h2::after {
content: '' / '';
display: block;
inline-size: min(2.4em, 100%);
block-size: 5px;
margin-block-start: 0.2em;
border-radius: 3px;
background: var(--chapter-accent, var(--accent));
box-shadow: 0 0 22px color-mix(in srgb, var(--chapter-accent, var(--accent)) 70%, transparent);
}
.reading-stage .portfolio-section > .section-eyebrow {
margin-block-end: 0.9rem;
text-shadow: 0 0 10px rgb(9 4 22 / 90%);
}
/* The reading panel carries the chrome the whole section used to. */
.reading-stage .chapter-body {
position: relative;
padding: clamp(28px, 3.4vw, 56px);
border: 1px solid color-mix(in srgb, var(--chapter-accent, var(--accent)) 30%, transparent);
border-radius: var(--radius-surface);
background:
radial-gradient(ellipse at 100% 0%, rgb(var(--chapter-glow, 40 70 110) / 18%), transparent 65%),
var(--surface);
box-shadow:
var(--panel-shadow),
inset 0 1px color-mix(in srgb, var(--chapter-accent, var(--accent)) 42%, transparent);
}
.reading-stage .chapter-body > * + * {
margin-block-start: var(--space-3);
}
.reading-stage [data-layout='right-rail'] .chapter-body > p {
font-size: clamp(19px, 1.5vw, 24px);
line-height: 1.6;
letter-spacing: -0.02em;
}
@media (prefers-reduced-motion: no-preference) {
.reading-stage .portfolio-section[data-current] .chapter-body {
animation: exit-panel-light 1.6s cubic-bezier(0.2, 0.75, 0.2, 1) both;
}
}
/* End of chapter: the next exit, signed like a roadside advance sign. */
.reading-stage .portfolio-section[data-exit]::after {
content: 'NEXT EXIT ' attr(data-next-exit) '\A' attr(data-next) ' \2192' / '';
inset-block: auto clamp(36px, 6vh, 76px);
inset-inline: auto 0;
translate: none;
padding: 0.85rem 1.15rem 0.8rem;
border: 2px solid color-mix(in srgb, var(--chapter-accent, var(--accent)) 70%, transparent);
border-radius: 6px;
background: rgb(12 6 28 / 90%);
box-shadow: 0 0 28px color-mix(in srgb, var(--chapter-accent, var(--accent)) 22%, transparent);
color: var(--text);
font: 640 0.8rem/1.55 var(--mono);
letter-spacing: 0.14em;
text-align: start;
}
.reading-stage .portfolio-section[data-layout='left-timeline'][data-exit]::after {
inset-inline: 0 auto;
}
.reading-stage .portfolio-section[data-exit='08']::after {
content: 'END OF ROUTE' / '';
}
.reading-stage .portfolio-section[data-exit='08'][data-trip]::after {
content: 'END OF ROUTE\A' attr(data-trip) / '';
}
}
/* ---------- Destination: the ending is a place, not a small card ---------- */
@media screen and (min-width: 768px) {
.reading-stage .portfolio-section[data-layout='destination'] {
inline-size: min(900px, 64%);
margin-block-end: clamp(80px, 12vh, 140px);
}
.reading-stage [data-layout='destination'] > h2 {
font-size: clamp(72px, 10vw, 168px);
}
.reading-stage [data-layout='destination'] .contact-destination__copy {
font-size: clamp(26px, 2.4vw, 38px);
line-height: 1.22;
letter-spacing: -0.03em;
}
.reading-stage [data-layout='destination'] .contact-link {
min-block-size: 56px;
padding-inline: 1.6rem;
font-size: 1.05rem;
}
}
/* ---------- Case studies: evidence beside media that stays in view ---------- */
@media screen and (min-width: 1100px) {
.reading-stage .project-case-study[data-media-layout]:not([data-media-layout='none']) {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: clamp(32px, 3.6vw, 72px);
align-items: start;
}
.reading-stage .project-case-study[data-media-layout]:not([data-media-layout='none']) > :not(.project-media-gallery) {
grid-column: 1;
}
.reading-stage .project-case-study[data-media-layout]:not([data-media-layout='none']) > .project-media-gallery {
grid-column: 2;
grid-row: 1 / span 3;
position: sticky;
inset-block-start: calc(var(--sign-reading-top, 220px) + 16px);
margin-block: 0;
}
/* Evidence reads as a spec sheet: the lettered stop in a rail, text beside it. */
.reading-stage .project-evidence > div {
display: grid;
grid-template-columns: minmax(9rem, 0.34fr) minmax(0, 1fr);
column-gap: 1.4rem;
align-items: baseline;
}
.reading-stage .project-case-study[data-media-layout]:not([data-media-layout='none']) .project-evidence {
grid-template-columns: minmax(0, 1fr);
}
.reading-stage .project-case-study[data-media-layout]:not([data-media-layout='none']) .project-evidence ul {
columns: auto;
}
}
/* ---------- Route map navigation ---------- */
@media screen and (min-width: 768px) {
.site-navigation ul {
position: relative;
}
/* The road: a thin line under the exits, lit up to the current one. */
.site-navigation ul::before,
.site-navigation ul::after {
content: '' / '';
position: absolute;
inset-block-end: 3px;
inset-inline-start: calc(100% / var(--route-count, 8) / 2);
block-size: 2px;
border-radius: 2px;
pointer-events: none;
}
.site-navigation ul::before {
inline-size: calc(100% - 100% / var(--route-count, 8));
background: rgb(198 122 209 / 22%);
}
.site-navigation ul::after {
inline-size: calc((100% - 100% / var(--route-count, 8)) * var(--route-index, 0) / (var(--route-count, 8) - 1));
background: linear-gradient(90deg, var(--rose, #ff83c2), var(--violet, #c4a2ff), var(--accent), var(--coral, #ffa68d));
box-shadow: 0 0 10px rgb(255 131 194 / 45%);
transition: inline-size 520ms cubic-bezier(0.22, 1, 0.36, 1);
}
.site-navigation li {
position: relative;
}
/* Exit nodes on the road: hollow ahead, filled once passed, the car marker here. */
.site-navigation li::after {
content: '' / '';
position: absolute;
z-index: 1;
inset-block-end: 0;
inset-inline-start: 50%;
inline-size: 8px;
block-size: 8px;
translate: -50% 0;
border: 1.5px solid rgb(198 122 209 / 60%);
border-radius: 50%;
background: rgb(22 12 40);
pointer-events: none;
transition: background-color 300ms, border-color 300ms;
}
.site-navigation li[data-route='passed']::after {
border-color: var(--rose, #ff83c2);
background: var(--rose, #ff83c2);
}
.site-navigation li[data-route='here']::after {
inline-size: 14px;
block-size: 8px;
inset-block-end: 0;
border: 0;
border-radius: 4px 4px 2px 2px;
background: var(--focus);
box-shadow: 0 0 12px var(--focus), -6px 0 10px -2px rgb(255 214 147 / 55%);
}
.site-navigation[data-motion='false'] ul::after,
.site-navigation[data-motion='false'] li::after {
transition: none;
}
}
/* Compact screens: the closed summary names where you are on the route. */
@media screen and (max-width: 767px) {
.site-navigation summary[data-exit]::after {
content: 'EXIT ' attr(data-exit) ' \00b7 ' attr(data-label) / '';
margin-inline-start: 0.7em;
padding: 0.2rem 0.45rem;
border-radius: 3px;
background: rgb(255 131 194 / 16%);
color: var(--text);
font: 640 0.66rem/1.3 var(--mono);
letter-spacing: 0.12em;
white-space: pre;
}
/* The open list is drawn as a single vertical route with exit nodes. */
.site-navigation ul {
position: relative;
grid-template-columns: minmax(0, 1fr);
}
.site-navigation li {
position: relative;
}
.site-navigation li::before {
content: '' / '';
position: absolute;
inset-block: 0;
inset-inline-start: 10px;
inline-size: 2px;
background: rgb(198 122 209 / 28%);
pointer-events: none;
}
.site-navigation li::after {
content: '' / '';
position: absolute;
inset-block-start: 50%;
inset-inline-start: 6px;
inline-size: 10px;
block-size: 10px;
translate: 0 -50%;
border: 1.5px solid rgb(198 122 209 / 70%);
border-radius: 50%;
background: rgb(22 12 40);
pointer-events: none;
}
.site-navigation li[data-route='passed']::after {
background: var(--rose, #ff83c2);
border-color: var(--rose, #ff83c2);
}
.site-navigation li[data-route='here']::after {
background: var(--focus);
border-color: var(--focus);
box-shadow: 0 0 10px var(--focus);
}
.site-navigation li a {
justify-content: flex-start;
padding-inline-start: 30px;
text-align: start;
}
}
/* ---------- Resume chip for returning visitors ---------- */
.hero-journey__resume {
display: inline-flex;
align-items: center;
gap: 0.6rem;
inline-size: fit-content;
min-block-size: 44px;
margin-block-start: 0.9rem;
padding: 0.5rem 0.9rem;
border: 1px solid rgb(255 214 147 / 45%);
border-radius: 999px;
background: rgb(14 7 30 / 78%);
color: var(--text);
font: 600 0.74rem/1 var(--mono);
letter-spacing: 0.12em;
text-decoration: none;
}
.hero-journey__resume::before {
content: 'RESUME' / '';
color: var(--focus);
}
.hero-journey__resume::after {
content: 'EXIT ' attr(data-exit) ' \00b7 ' attr(data-label) ' \2192' / '';
white-space: pre;
}
.hero-journey__resume:hover {
border-color: var(--focus);
}
.hero-journey__resume:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 3px;
}
/* ---------- Forced colours, reduced motion, print ---------- */
@media (forced-colors: active) {
.reading-stage .chapter-body {
border: 1px solid CanvasText;
background: Canvas;
}
.reading-stage :is(.portfolio-section, .project-stage__lead) > h2 {
text-shadow: none;
}
.reading-stage :is(.portfolio-section, .project-stage__lead) > h2::after,
.site-navigation ul::before,
.site-navigation ul::after,
.site-navigation li::before,
.site-navigation li::after {
display: none;
}
.reading-stage .portfolio-section[data-exit]::after,
.hero-journey__resume {
forced-color-adjust: none;
border: 1px solid CanvasText;
background: Canvas;
color: CanvasText;
}
}
@media print {
.hero-journey__resume,
.site-navigation summary[data-exit]::after {
display: none !important;
}
}

View File

@@ -807,6 +807,7 @@ test('keeps every passive transition sign clear of visible opaque panels and fix
[ [
'.hero-journey__content', '.hero-journey__content',
'.portfolio-section:not([data-layout="full-diagram"]):not([data-layout="project-stage"])', '.portfolio-section:not([data-layout="full-diagram"]):not([data-layout="project-stage"])',
'.chapter-body',
'.portfolio-section[data-layout="full-diagram"] > h2', '.portfolio-section[data-layout="full-diagram"] > h2',
'.infrastructure-case > h3', '.infrastructure-case > h3',
'.infrastructure-case > p', '.infrastructure-case > p',