feat: style
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
This repository is an Angular 21 SSR application. Main app code lives in `src/`,
|
This repository is an Angular 21 SSR application. Main app code lives in `src/`,
|
||||||
with standalone components under `src/app/` such as `home/`, `footer/`, and
|
with standalone components under `src/app/` such as `home/`, `footer/`, and
|
||||||
`imprint/`. Shared constants are kept in `src/global.fields.ts`. Component tests
|
`imprint/`. Shared constants are kept in `src/global.ts`. Component tests
|
||||||
sit next to their source files as `*.spec.ts`. Automation and project
|
sit next to their source files as `*.spec.ts`. Automation and project
|
||||||
maintenance scripts live in `scripts/`. Generated build output goes to `dist/`
|
maintenance scripts live in `scripts/`. Generated build output goes to `dist/`
|
||||||
and should not be edited manually.
|
and should not be edited manually.
|
||||||
@@ -42,7 +42,7 @@ tests for changed behavior.
|
|||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
Recent history mixes short imperative messages (`Update src/global.fields.ts`)
|
Recent history mixes short imperative messages (`Update src/global.ts`)
|
||||||
with lightweight conventional commits
|
with lightweight conventional commits
|
||||||
(`feat: pre-render routes and restore scroll`). Prefer concise, imperative
|
(`feat: pre-render routes and restore scroll`). Prefer concise, imperative
|
||||||
commit subjects and include a scope or prefix when it adds clarity. PRs should
|
commit subjects and include a scope or prefix when it adds clarity. PRs should
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import type { Global } from '../../global.fields';
|
import type { Global } from '../../global';
|
||||||
import { global } from '../../global.fields';
|
import { global } from '../../global';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-footer',
|
selector: 'app-footer',
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<section class="hero-section panel">
|
<section class="hero-section panel">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<div class="hero-copy-main">
|
<div class="hero-copy-main">
|
||||||
<p class="eyebrow">Portfolio</p>
|
<p id="portfolio-eyebrow" class="eyebrow">Portfolio</p>
|
||||||
<h1>
|
<h1>
|
||||||
<span class="hero-name-part">{{ global.firstname }}</span>
|
<span class="hero-name-part" [attr.data-name]="global.firstname">{{ global.firstname }}</span>
|
||||||
<span class="hero-name-part">{{ global.lastname }}</span>
|
<span class="hero-name-part" [attr.data-name]="global.lastname">{{ global.lastname }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero-intro">
|
<p class="hero-intro">
|
||||||
Full-stack development, thoughtful interfaces, and tailored digital products for companies, associations, and private clients.
|
Full-stack development, thoughtful interfaces, and tailored digital products for companies, associations, and private clients.
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
|
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<div>
|
<div>
|
||||||
<span class="meta-label">my stack</span>
|
<span class="meta-label">{{ currentBioEntry.label }}</span>
|
||||||
<strong>{{ global.bioTextsList[currentIndex] }}</strong>
|
<strong>{{ currentBioEntry.value }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,6 @@
|
|||||||
<section id="about" class="story-section panel">
|
<section id="about" class="story-section panel">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<p class="eyebrow">About me</p>
|
<p class="eyebrow">About me</p>
|
||||||
<h2>Direct communication, solid implementation, and software that stays understandable.</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="story-grid">
|
<div class="story-grid">
|
||||||
@@ -120,9 +119,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="projects" class="projects-section">
|
<section id="projects" class="projects-section">
|
||||||
<div class="section-heading">
|
<div class="section-heading section-heading-centered">
|
||||||
<p class="eyebrow">Selected Projects</p>
|
<p class="eyebrow">Projects</p>
|
||||||
<h2>A compact look at products, platforms, and systems I have helped shape.</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-list">
|
<div class="project-list">
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import type { SafeUrl } from '@angular/platform-browser';
|
|||||||
import type { Subscription } from 'rxjs';
|
import type { Subscription } from 'rxjs';
|
||||||
import { interval } from 'rxjs';
|
import { interval } from 'rxjs';
|
||||||
|
|
||||||
import type { Global, PortraitHighlight, PortfolioProject } from '../../global.fields';
|
import type { BioTextEntry, Global, PortraitHighlight, PortfolioProject } from '../../global';
|
||||||
import { global } from '../../global.fields';
|
import { global } from '../../global';
|
||||||
import { FooterComponent } from '../footer/footer.component';
|
import { FooterComponent } from '../footer/footer.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,6 +23,31 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
private readonly sanitizer = inject(DomSanitizer);
|
private readonly sanitizer = inject(DomSanitizer);
|
||||||
private readonly zone = inject(NgZone);
|
private readonly zone = inject(NgZone);
|
||||||
private preloadedImages: HTMLImageElement[] = [];
|
private preloadedImages: HTMLImageElement[] = [];
|
||||||
|
private tallestPortraitAspectRatio: number | null = null;
|
||||||
|
private lockedHeroSectionMinHeight: number = 0;
|
||||||
|
private scrollAnimationFrameId: number | null = null;
|
||||||
|
private heroHeightAnimationFrameId: number | null = null;
|
||||||
|
private readonly scheduleNameGradientUpdate: () => void = (): void => {
|
||||||
|
if (this.scrollAnimationFrameId !== null || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollAnimationFrameId = window.requestAnimationFrame((): void => {
|
||||||
|
this.scrollAnimationFrameId = null;
|
||||||
|
this.updateNameGradientProgress();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
private readonly scheduleHeroSectionHeightUpdate: () => void = (): void => {
|
||||||
|
if (this.heroHeightAnimationFrameId !== null || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.heroHeightAnimationFrameId = window.requestAnimationFrame((): void => {
|
||||||
|
this.heroHeightAnimationFrameId = null;
|
||||||
|
this.lockedHeroSectionMinHeight = 0;
|
||||||
|
this.updateHeroSectionMinHeight();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
protected readonly global: Global = global;
|
protected readonly global: Global = global;
|
||||||
protected readonly age: number | string = this.calculateAge(global.birthdate);
|
protected readonly age: number | string = this.calculateAge(global.birthdate);
|
||||||
@@ -43,6 +68,7 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected currentIndex = 0;
|
protected currentIndex = 0;
|
||||||
protected readonly projects: PortfolioProject[] = global.projects;
|
protected readonly projects: PortfolioProject[] = global.projects;
|
||||||
|
protected readonly fallbackBioEntry: BioTextEntry = { label: 'my stack', value: '' };
|
||||||
private sub!: Subscription;
|
private sub!: Subscription;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -51,6 +77,7 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.preloadImageAssets();
|
this.preloadImageAssets();
|
||||||
|
this.initNameGradientScrollAnimation();
|
||||||
|
|
||||||
this.zone.runOutsideAngular((): void => {
|
this.zone.runOutsideAngular((): void => {
|
||||||
this.sub = interval(1000).subscribe((): void => {
|
this.sub = interval(1000).subscribe((): void => {
|
||||||
@@ -63,6 +90,7 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.sub.unsubscribe();
|
this.sub.unsubscribe();
|
||||||
|
this.destroyNameGradientScrollAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toggleBio(): void {
|
protected toggleBio(): void {
|
||||||
@@ -73,6 +101,10 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
return this.portraitHighlights[this.currentPortraitHighlightIndex] ?? null;
|
return this.portraitHighlights[this.currentPortraitHighlightIndex] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get currentBioEntry(): BioTextEntry {
|
||||||
|
return this.global.bioTextsList[this.currentIndex] ?? this.fallbackBioEntry;
|
||||||
|
}
|
||||||
|
|
||||||
protected showNextPortraitHighlight(): void {
|
protected showNextPortraitHighlight(): void {
|
||||||
if (this.portraitHighlights.length <= 1 || this.isPortraitSwitching) {
|
if (this.portraitHighlights.length <= 1 || this.isPortraitSwitching) {
|
||||||
return;
|
return;
|
||||||
@@ -113,6 +145,158 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
image.src = url;
|
image.src = url;
|
||||||
return image;
|
return image;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.syncHeroHeightToLargestPortraitImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncHeroHeightToLargestPortraitImage(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const portraitImageUrls: Set<string> = new Set<string>(
|
||||||
|
this.portraitHighlights.map((highlight: PortraitHighlight): string => new URL(highlight.image, window.location.href).href),
|
||||||
|
);
|
||||||
|
|
||||||
|
const portraitImages: HTMLImageElement[] = this.preloadedImages.filter((image: HTMLImageElement): boolean =>
|
||||||
|
portraitImageUrls.has(image.src),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (portraitImages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalizeAspectRatio = (): void => {
|
||||||
|
const minAspectRatio: number = portraitImages.reduce((minRatio: number, image: HTMLImageElement): number => {
|
||||||
|
if (image.naturalWidth === 0 || image.naturalHeight === 0) {
|
||||||
|
return minRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentRatio: number = image.naturalWidth / image.naturalHeight;
|
||||||
|
return minRatio === 0 ? currentRatio : Math.min(minRatio, currentRatio);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (minAspectRatio > 0) {
|
||||||
|
this.tallestPortraitAspectRatio = minAspectRatio;
|
||||||
|
this.scheduleHeroSectionHeightUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pendingLoads: number = portraitImages.length;
|
||||||
|
const onImageSettled = (): void => {
|
||||||
|
pendingLoads -= 1;
|
||||||
|
if (pendingLoads === 0) {
|
||||||
|
finalizeAspectRatio();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const image of portraitImages) {
|
||||||
|
if (image.complete) {
|
||||||
|
onImageSettled();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.addEventListener('load', onImageSettled, { once: true });
|
||||||
|
image.addEventListener('error', onImageSettled, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initNameGradientScrollAnimation(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', this.scheduleNameGradientUpdate, { passive: true });
|
||||||
|
window.addEventListener('resize', this.scheduleNameGradientUpdate, { passive: true });
|
||||||
|
window.addEventListener('resize', this.scheduleHeroSectionHeightUpdate, { passive: true });
|
||||||
|
this.scheduleNameGradientUpdate();
|
||||||
|
this.scheduleHeroSectionHeightUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroyNameGradientScrollAnimation(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('scroll', this.scheduleNameGradientUpdate);
|
||||||
|
window.removeEventListener('resize', this.scheduleNameGradientUpdate);
|
||||||
|
window.removeEventListener('resize', this.scheduleHeroSectionHeightUpdate);
|
||||||
|
|
||||||
|
if (this.scrollAnimationFrameId !== null) {
|
||||||
|
window.cancelAnimationFrame(this.scrollAnimationFrameId);
|
||||||
|
this.scrollAnimationFrameId = null;
|
||||||
|
}
|
||||||
|
if (this.heroHeightAnimationFrameId !== null) {
|
||||||
|
window.cancelAnimationFrame(this.heroHeightAnimationFrameId);
|
||||||
|
this.heroHeightAnimationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.style.removeProperty('--name-pride-progress');
|
||||||
|
document.documentElement.style.removeProperty('--hero-section-min-height');
|
||||||
|
this.lockedHeroSectionMinHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateNameGradientProgress(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root: HTMLElement = document.documentElement;
|
||||||
|
const portfolioEyebrow: HTMLElement | null = document.getElementById('portfolio-eyebrow');
|
||||||
|
const portfolioTriggerY: number =
|
||||||
|
portfolioEyebrow === null
|
||||||
|
? Number.POSITIVE_INFINITY
|
||||||
|
: portfolioEyebrow.getBoundingClientRect().top + window.scrollY;
|
||||||
|
|
||||||
|
const triggerDistance: number = Math.max(portfolioTriggerY, 1);
|
||||||
|
const rawProgress: number = Math.min(window.scrollY / triggerDistance, 1);
|
||||||
|
const acceleratedProgress: number = Math.pow(rawProgress, 0.66);
|
||||||
|
const progress: number =
|
||||||
|
window.scrollY > 0 ? Math.max(acceleratedProgress, 0.05) : acceleratedProgress;
|
||||||
|
|
||||||
|
root.style.setProperty('--name-pride-progress', progress.toFixed(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateHeroSectionMinHeight(): void {
|
||||||
|
if (typeof window === 'undefined' || this.tallestPortraitAspectRatio === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroSection: HTMLElement | null = document.querySelector('.hero-section');
|
||||||
|
const heroCopy: HTMLElement | null = document.querySelector('.hero-copy');
|
||||||
|
const portraitHighlight: HTMLElement | null = document.querySelector('.portrait-highlight');
|
||||||
|
const portraitButton: HTMLElement | null = document.querySelector('.portrait-highlight-button');
|
||||||
|
|
||||||
|
if (
|
||||||
|
heroSection === null ||
|
||||||
|
heroCopy === null ||
|
||||||
|
portraitHighlight === null ||
|
||||||
|
portraitButton === null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonWidth: number = portraitButton.getBoundingClientRect().width;
|
||||||
|
if (buttonWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxMediaHeight: number = buttonWidth / this.tallestPortraitAspectRatio;
|
||||||
|
const currentButtonHeight: number = portraitButton.getBoundingClientRect().height;
|
||||||
|
const currentHighlightHeight: number = portraitHighlight.getBoundingClientRect().height;
|
||||||
|
const staticHighlightOverhead: number = Math.max(currentHighlightHeight - currentButtonHeight, 0);
|
||||||
|
const requiredHighlightHeight: number = maxMediaHeight + staticHighlightOverhead;
|
||||||
|
const requiredContentHeight: number = Math.max(heroCopy.getBoundingClientRect().height, requiredHighlightHeight);
|
||||||
|
const heroSectionStyles: CSSStyleDeclaration = window.getComputedStyle(heroSection);
|
||||||
|
const paddingTop: number = Number.parseFloat(heroSectionStyles.paddingTop) || 0;
|
||||||
|
const paddingBottom: number = Number.parseFloat(heroSectionStyles.paddingBottom) || 0;
|
||||||
|
const requiredSectionMinHeight: number = requiredContentHeight + paddingTop + paddingBottom;
|
||||||
|
this.lockedHeroSectionMinHeight = Math.max(this.lockedHeroSectionMinHeight, requiredSectionMinHeight);
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--hero-section-min-height',
|
||||||
|
`${Math.ceil(this.lockedHeroSectionMinHeight)}px`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateAge(birthDateString: string): number | string {
|
private calculateAge(birthDateString: string): number | string {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type { IconDefinition } from '@fortawesome/angular-fontawesome';
|
|||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import type { Global } from '../../global.fields';
|
import type { Global } from '../../global';
|
||||||
import { global } from '../../global.fields';
|
import { global } from '../../global';
|
||||||
import { FooterComponent } from '../footer/footer.component';
|
import { FooterComponent } from '../footer/footer.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -28,12 +28,17 @@ export interface Global {
|
|||||||
contactPhone: string;
|
contactPhone: string;
|
||||||
hrefContactPhone: string;
|
hrefContactPhone: string;
|
||||||
birthdate: string;
|
birthdate: string;
|
||||||
bioTextsList: string[];
|
bioTextsList: BioTextEntry[];
|
||||||
portraitHighlights: PortraitHighlight[];
|
portraitHighlights: PortraitHighlight[];
|
||||||
projects: PortfolioProject[];
|
projects: PortfolioProject[];
|
||||||
address: ImprintData;
|
address: ImprintData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BioTextEntry {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const global: Global = {
|
export const global: Global = {
|
||||||
firstname: 'Julian',
|
firstname: 'Julian',
|
||||||
lastname: 'Lechner',
|
lastname: 'Lechner',
|
||||||
@@ -43,27 +48,26 @@ export const global: Global = {
|
|||||||
hrefContactPhone: '+436609254001',
|
hrefContactPhone: '+436609254001',
|
||||||
birthdate: '2009-03-03',
|
birthdate: '2009-03-03',
|
||||||
bioTextsList: [
|
bioTextsList: [
|
||||||
'C#',
|
{ label: 'Languages', value: 'C#' },
|
||||||
'.NET',
|
{ label: 'Languages', value: 'TypeScript' },
|
||||||
'.NET Framework',
|
{ label: 'Languages', value: 'Bash' },
|
||||||
'WPF',
|
{ label: 'Frontend', value: 'Angular' },
|
||||||
'Avalonia',
|
{ label: 'Frontend', value: 'WPF' },
|
||||||
'NodeJS',
|
{ label: 'Frontend', value: 'Avalonia' },
|
||||||
'Angular',
|
{ label: 'Backend', value: '.NET' },
|
||||||
'TypeScript',
|
{ label: 'Backend', value: 'ASP.NET Core' },
|
||||||
'JavaScript',
|
{ label: 'Backend', value: 'Entity Framework Core' },
|
||||||
'Java',
|
{ label: 'Backend', value: 'Node.js' },
|
||||||
'HTML',
|
{ label: 'Backend', value: 'REST' },
|
||||||
'(S)CSS',
|
{ label: 'Backend', value: 'Swagger' },
|
||||||
'Github',
|
{ label: 'Database', value: 'Microsoft SQL' },
|
||||||
'Gitlab',
|
{ label: 'Database', value: 'MariaDB' },
|
||||||
'Azure DevOps',
|
{ label: 'DevOps', value: 'Git' },
|
||||||
'Ubuntu',
|
{ label: 'DevOps', value: 'GitHub & GitLab' },
|
||||||
'Debian',
|
{ label: 'DevOps', value: 'Azure DevOps' },
|
||||||
'SSMS',
|
{ label: 'DevOps', value: 'Docker' },
|
||||||
'MariaDB',
|
{ label: 'DevOps', value: 'Ubuntu' },
|
||||||
'MySQL',
|
{ label: 'DevOps', value: 'Arch' },
|
||||||
'Grandle',
|
|
||||||
],
|
],
|
||||||
portraitHighlights: [
|
portraitHighlights: [
|
||||||
{
|
{
|
||||||
+32
-7
@@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
--page-max-width: 1160px;
|
--page-max-width: 1000px;
|
||||||
--content-max-width: 920px;
|
--content-max-width: 920px;
|
||||||
--surface: rgba(20, 18, 22, 0.62);
|
--surface: rgba(20, 18, 22, 0.62);
|
||||||
--surface-strong: rgba(15, 14, 18, 0.78);
|
--surface-strong: rgba(15, 14, 18, 0.78);
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
--space-7: 4.5rem;
|
--space-7: 4.5rem;
|
||||||
--transition-fast: 180ms ease;
|
--transition-fast: 180ms ease;
|
||||||
--transition-medium: 320ms ease;
|
--transition-medium: 320ms ease;
|
||||||
|
--name-pride-progress: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -68,7 +69,7 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-shell {
|
.page-shell {
|
||||||
width: min(calc(100% - 2rem), var(--page-max-width));
|
width: min(calc(100% - 6rem), var(--page-max-width));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--space-6) 0 var(--space-7);
|
padding: var(--space-6) 0 var(--space-7);
|
||||||
}
|
}
|
||||||
@@ -94,6 +95,11 @@ hr {
|
|||||||
max-width: 44rem;
|
max-width: 44rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-heading-centered {
|
||||||
|
margin-inline: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading.compact {
|
.section-heading.compact {
|
||||||
max-width: 52rem;
|
max-width: 52rem;
|
||||||
}
|
}
|
||||||
@@ -111,7 +117,7 @@ hr {
|
|||||||
grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr);
|
grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr);
|
||||||
gap: var(--space-6);
|
gap: var(--space-6);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
min-height: min(86svh, 780px);
|
min-height: var(--hero-section-min-height, auto);
|
||||||
padding: clamp(2rem, 4vw, 4rem);
|
padding: clamp(2rem, 4vw, 4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +128,27 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-name-part {
|
.hero-name-part {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 0.28em;
|
margin-right: 0.28em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name-part::after {
|
||||||
|
content: attr(data-name);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name-part::after {
|
||||||
|
--name-pride-reveal: clamp(0, var(--name-pride-progress), 1);
|
||||||
|
width: calc(var(--name-pride-reveal) * (100% + 0.14em));
|
||||||
|
overflow: hidden;
|
||||||
|
color: transparent;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
#e40303 0%,
|
#e40303 0%,
|
||||||
@@ -141,9 +166,10 @@ hr {
|
|||||||
);
|
);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
|
background-position: 0 50%;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-name-part:last-child {
|
.hero-name-part:last-child {
|
||||||
@@ -623,7 +649,6 @@ hr {
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +753,7 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-inner {
|
.footer-inner {
|
||||||
width: min(calc(100% - 2rem), var(--page-max-width));
|
width: min(calc(100% - 6rem), var(--page-max-width));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-top: var(--space-4);
|
padding-top: var(--space-4);
|
||||||
border-top: 1px solid var(--border-soft);
|
border-top: 1px solid var(--border-soft);
|
||||||
@@ -785,7 +810,7 @@ hr {
|
|||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.page-shell {
|
.page-shell {
|
||||||
width: min(calc(100% - 2rem), var(--page-max-width));
|
width: min(calc(100% - 4rem), var(--page-max-width));
|
||||||
padding: var(--space-4) 0 var(--space-6);
|
padding: var(--space-4) 0 var(--space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user