diff --git a/src/App.vue b/src/App.vue index 8914b64..8bdce52 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,6 +11,7 @@ import './styles/chromatic.css'; import './styles/refinements.css'; import './styles/title-navigation.css'; import './styles/wayfinding.css'; +import './styles/route.css'; import './styles/print.css'; defineProps<{ inputs: Readonly }>(); diff --git a/src/components/PortfolioSection.vue b/src/components/PortfolioSection.vue index a2f2fac..977cdcf 100644 --- a/src/components/PortfolioSection.vue +++ b/src/components/PortfolioSection.vue @@ -10,11 +10,13 @@ defineProps<{ exit?: string | undefined; /** Next chapter label for the plate's "then" line (decorative, CSS-generated). */ nextLabel?: string | undefined; + /** Next chapter's exit number, for the end-of-chapter "next exit" sign. */ + nextExit?: string | undefined; }>(); - + +
diff --git a/src/styles/components.css b/src/styles/components.css index 0d7a738..e3d14e8 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -98,6 +98,7 @@ } .portfolio-section > p, +.portfolio-section > .chapter-body > p, .portfolio-section article p, .portfolio-section dd, .portfolio-section li { @@ -105,6 +106,7 @@ } .portfolio-section > * + *, +.portfolio-section > .chapter-body > * + *, .portfolio-section article > * + *, .portfolio-section li + li { margin-block-start: var(--space-2); diff --git a/src/styles/evolution.css b/src/styles/evolution.css index 38f7bc0..183e2bf 100644 --- a/src/styles/evolution.css +++ b/src/styles/evolution.css @@ -166,7 +166,7 @@ body { font-size: 0.7rem; 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); line-height: 1.6; letter-spacing: -0.02em; @@ -635,7 +635,7 @@ body { .portfolio-section[data-layout='project-stage'] > .section-eyebrow { margin-inline: 12px; } - .portfolio-section[data-layout='right-rail'] > p { + .portfolio-section[data-layout='right-rail'] > .chapter-body > p { font-size: 20px; } .experience-record { diff --git a/src/styles/route.css b/src/styles/route.css new file mode 100644 index 0000000..cb5d2ea --- /dev/null +++ b/src/styles/route.css @@ -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; + } +} diff --git a/tests/browser/journey-integration.spec.ts b/tests/browser/journey-integration.spec.ts index 5d12556..57be334 100644 --- a/tests/browser/journey-integration.spec.ts +++ b/tests/browser/journey-integration.spec.ts @@ -807,6 +807,7 @@ test('keeps every passive transition sign clear of visible opaque panels and fix [ '.hero-journey__content', '.portfolio-section:not([data-layout="full-diagram"]):not([data-layout="project-stage"])', + '.chapter-body', '.portfolio-section[data-layout="full-diagram"] > h2', '.infrastructure-case > h3', '.infrastructure-case > p',