Fix homepage quality gate

This commit is contained in:
codex 2026-06-29 14:49:49 -03:00
parent 0437bfe9b3
commit 381ecb4ea0
8 changed files with 1393 additions and 1143 deletions

View File

@ -0,0 +1,223 @@
<template>
<section id="contact" class="conversion-section" aria-labelledby="contact-title">
<div class="conversion-card final-contact">
<div>
<p class="section-kicker mono">Contact</p>
<h2 id="contact-title">{{ content.contactSection.heading }}</h2>
<p>{{ content.contactSection.description }}</p>
<p>
Recruiting for a remote engineering role? Send the role description, contract terms, location restrictions, and
expected timeline.
</p>
</div>
<div class="contact-actions action-card action-stack" aria-label="Contact actions">
<a class="btn primary-action" :href="mailtoHref">Email Brad</a>
<a class="btn secondary" :href="content.linkedInUrl" target="_blank" rel="noopener noreferrer">
Connect on LinkedIn
</a>
<button class="btn tertiary button-like" type="button" @click="copyEmail">
{{ emailCopied ? "Email Copied" : "Copy Email Address" }}
</button>
</div>
</div>
</section>
<footer class="site-footer">
<div>
<div class="footer-name">{{ content.professionalName }}</div>
<div>{{ content.role }}</div>
</div>
<nav aria-label="Footer navigation">
<a :href="mailtoHref">Email</a>
<a :href="content.linkedInUrl" target="_blank" rel="noopener noreferrer">LinkedIn</a>
<RouterLink :to="content.resumeUrl">R&eacute;sum&eacute;</RouterLink>
<a :href="content.sourceUrl" target="_blank" rel="noopener noreferrer">Public Source</a>
<a href="#platform">Platform</a>
<button type="button" @click="doLogin">Sign In</button>
<RouterLink to="/request-access">Request Lab Access</RouterLink>
</nav>
<div class="copyright mono">&copy; {{ currentYear }} Brad Stein</div>
</footer>
</template>
<script setup>
import { ref } from "vue";
import { RouterLink } from "vue-router";
import { login } from "../auth";
const props = defineProps({
content: Object,
mailtoHref: String,
currentYear: Number,
});
const emailCopied = ref(false);
function doLogin() {
login();
}
async function copyEmail() {
try {
await navigator.clipboard?.writeText(props.content.primaryContact.email);
emailCopied.value = true;
window.setTimeout(() => {
emailCopied.value = false;
}, 1500);
} catch {
emailCopied.value = false;
}
}
</script>
<style scoped>
.conversion-section {
box-sizing: border-box;
display: grid;
align-content: start;
scroll-margin-top: 0;
margin-top: 0;
padding: clamp(54px, 7vh, 82px) 0;
}
.conversion-card {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(300px, 0.7fr);
gap: 24px;
align-items: center;
padding: clamp(28px, 4vw, 42px);
}
.section-kicker {
color: var(--accent-cyan);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0 8px;
}
.conversion-card h2 {
margin: 0;
font-size: clamp(30px, 5vw, 46px);
letter-spacing: 0;
}
.conversion-card p {
font-size: 17px;
line-height: 1.65;
}
.final-contact {
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius);
background: linear-gradient(135deg, rgba(0, 229, 197, 0.08), rgba(120, 180, 255, 0.06));
padding: clamp(26px, 3.5vw, 38px);
}
#contact {
padding-top: clamp(42px, 6vh, 72px);
}
.contact-actions {
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.action-card {
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
background: rgba(5, 12, 28, 0.55);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.action-stack {
width: 100%;
max-width: 360px;
justify-self: end;
flex-direction: column;
flex-wrap: nowrap;
gap: 12px;
}
.action-stack .btn {
width: 100%;
justify-content: center;
text-align: center;
}
.primary-action {
background: linear-gradient(135deg, rgba(0, 229, 197, 0.9), rgba(120, 180, 255, 0.82));
color: #02141d;
}
.tertiary {
background: transparent;
color: var(--text-strong);
}
.button-like {
cursor: pointer;
font: inherit;
}
.site-footer {
display: grid;
grid-template-columns: 1fr;
gap: 14px;
margin-top: 56px;
padding-top: 28px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
color: var(--text-muted);
}
.footer-name {
color: var(--text-strong);
font-weight: 800;
}
.site-footer nav {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.site-footer button {
border: 0;
padding: 0;
background: transparent;
color: var(--accent-cyan);
cursor: pointer;
font: inherit;
}
.btn:focus-visible,
.site-footer a:focus-visible,
.site-footer button:focus-visible {
outline: 2px solid rgba(0, 229, 197, 0.75);
outline-offset: 3px;
}
@media (max-width: 1080px) {
.conversion-card {
grid-template-columns: 1fr;
}
.conversion-section {
min-height: auto;
}
.action-stack {
justify-self: stretch;
max-width: none;
}
}
@media (max-width: 760px) {
.conversion-section {
margin-top: 0;
padding: 50px 0;
}
}
</style>

View File

@ -0,0 +1,297 @@
<template>
<section class="hero-section" aria-labelledby="home-title">
<div class="hero-copy">
<p class="eyebrow mono">{{ content.eyebrow }}</p>
<h1 id="home-title">{{ content.professionalName }}</h1>
<h2>{{ content.role }}</h2>
<p class="hero-summary">{{ content.summary }}</p>
<p class="experience-line">{{ content.experienceLine }}</p>
<p class="availability-line">{{ content.availability.label }}</p>
<div class="cta-row" aria-label="Professional actions">
<a class="btn primary-action" :href="mailtoHref">Discuss a Project</a>
<RouterLink class="btn secondary" :to="content.resumeUrl">View R&eacute;sum&eacute;</RouterLink>
<a class="btn tertiary" href="#platform">Explore the Live Platform</a>
</div>
<div class="tech-list" aria-label="Core technologies">
<span v-for="tech in content.techStack" :key="tech" class="pill mono">{{ tech }}</span>
</div>
</div>
<aside class="status-card glass" aria-labelledby="status-title">
<div class="status-head">
<p class="eyebrow mono">Live platform</p>
<h3 id="status-title">Titan Lab Status</h3>
</div>
<div class="status-list">
<div v-for="item in statusItems" :key="item.label" class="status-row">
<span class="status-dot" :class="item.state" aria-hidden="true"></span>
<div>
<a v-if="item.labelHref" class="status-label status-title-link" :href="item.labelHref">
{{ item.label }}
</a>
<div v-else class="status-label">{{ item.label }}</div>
<a
v-if="item.href && isExternal(item.href)"
class="status-value mono status-link"
:href="item.href"
target="_blank"
rel="noopener noreferrer"
>
{{ item.value }}
</a>
<RouterLink v-else-if="item.href" class="status-value mono status-link" :to="item.href">
{{ item.value }}
</RouterLink>
<div v-else class="status-value mono">{{ item.value }}</div>
</div>
</div>
</div>
<p class="status-note mono">Last checked: {{ lastCheckedLabel }}</p>
<p v-if="error || (!loading && !labStatus?.connected)" class="status-note">
Live status is temporarily unavailable. The platform overview remains available below.
</p>
</aside>
</section>
</template>
<script setup>
import { RouterLink } from "vue-router";
defineProps({
content: Object,
statusItems: Array,
labStatus: Object,
loading: Boolean,
error: String,
lastCheckedLabel: String,
mailtoHref: String,
});
function isExternal(href) {
return typeof href === "string" && /^https?:\/\//.test(href);
}
</script>
<style scoped>
.hero-section {
position: relative;
isolation: isolate;
display: grid;
grid-template-columns: minmax(0, 1.75fr) minmax(320px, 0.85fr);
gap: 26px;
align-items: center;
min-height: calc(100vh - 86px);
padding: 42px 0 30px;
overflow: hidden;
}
.hero-section::before,
.hero-section::after {
content: "";
position: absolute;
pointer-events: none;
z-index: -1;
}
.hero-section::before {
inset: 16% -8vw 8% 50%;
border-radius: 999px;
background:
radial-gradient(ellipse at 68% 42%, rgba(0, 229, 197, 0.17), transparent 32%),
radial-gradient(ellipse at 28% 66%, rgba(120, 180, 255, 0.11), transparent 36%),
radial-gradient(ellipse at center, rgba(11, 24, 46, 0.58), transparent 72%);
opacity: 0.8;
filter: blur(2px);
}
.hero-section::after {
width: min(300px, 24vw);
top: 28%;
right: 6vw;
bottom: 22%;
border-radius: 999px;
background:
radial-gradient(ellipse at center, rgba(0, 229, 197, 0.16), transparent 64%),
linear-gradient(90deg, transparent, rgba(120, 180, 255, 0.08), transparent);
opacity: 0.32;
filter: blur(18px);
}
.hero-copy,
.status-card {
position: relative;
z-index: 1;
}
.eyebrow {
color: var(--accent-cyan);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0 8px;
}
.hero-copy h1 {
font-size: clamp(44px, 7vw, 72px);
line-height: 0.98;
margin: 0;
letter-spacing: 0;
}
.hero-copy h2 {
font-size: clamp(26px, 3.2vw, 36px);
margin: 12px 0 0;
color: var(--text-primary);
letter-spacing: 0;
}
.hero-summary {
max-width: 760px;
margin: 20px 0 0;
font-size: clamp(18px, 2.2vw, 22px);
line-height: 1.55;
color: var(--text-strong);
}
.experience-line,
.availability-line {
max-width: 760px;
margin: 14px 0 0;
font-size: 17px;
line-height: 1.65;
}
.availability-line {
color: rgba(170, 255, 215, 0.94);
}
.tech-list,
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tech-list {
margin-top: 22px;
}
.cta-row {
margin-top: 28px;
}
.primary-action {
background: linear-gradient(135deg, rgba(0, 229, 197, 0.9), rgba(120, 180, 255, 0.82));
color: #02141d;
}
.tertiary {
background: transparent;
color: var(--text-strong);
}
.status-card {
padding: 20px;
min-width: 0;
}
.status-head h3 {
margin: 0 0 14px;
font-size: 28px;
}
.status-list {
display: grid;
gap: 14px;
}
.status-row {
display: grid;
grid-template-columns: 14px 1fr;
gap: 12px;
align-items: start;
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.03);
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 999px;
margin-top: 4px;
background: var(--text-muted);
}
.status-dot.ok {
background: var(--accent-cyan);
}
.status-dot.bad {
background: var(--accent-rose);
}
.status-label {
font-weight: 800;
color: var(--text-strong);
}
.status-title-link {
display: inline-flex;
text-decoration: none;
}
.status-title-link:hover {
color: var(--accent-cyan);
}
.status-value,
.status-note {
color: var(--text-muted);
}
.status-link {
display: inline-flex;
color: var(--accent-cyan);
}
.status-note {
margin: 14px 0 0;
}
@media (min-width: 1180px) {
.hero-copy h2 {
white-space: nowrap;
}
}
@media (max-width: 1080px) {
.hero-section {
grid-template-columns: 1fr;
min-height: auto;
}
.hero-section::before {
inset: 96px -22vw 24px 18%;
opacity: 0.34;
}
.hero-section::after {
display: none;
}
}
@media (max-width: 760px) {
.hero-section {
padding-top: 28px;
}
.hero-section::before {
inset: 84px -44vw 18px 8%;
opacity: 0.28;
}
}
</style>

View File

@ -0,0 +1,391 @@
<template>
<section id="member-access" class="conversion-section" aria-labelledby="member-title">
<div class="conversion-card member-panel glass">
<div class="member-copy">
<p class="section-kicker mono">Member platform</p>
<h2 id="member-title">{{ content.memberAccessCopy.heading }}</h2>
<p>{{ content.memberAccessCopy.description }}</p>
<p v-if="memberPanelCopy" class="member-state-copy">{{ memberPanelCopy }}</p>
</div>
<div class="member-actions action-card" aria-live="polite" aria-label="Titan Lab member actions">
<div v-if="!auth.ready" class="member-loading">
<span class="skeleton"></span>
<span>Checking session...</span>
</div>
<template v-else-if="auth.authenticated">
<RouterLink class="btn primary-action" :to="memberAction?.to || '/apps'">
{{ memberAction?.label || "Open Services" }}
</RouterLink>
<RouterLink class="btn secondary" to="/account">Account</RouterLink>
<button class="btn tertiary button-like" type="button" @click="doLogout">Sign Out</button>
<p v-if="memberState.error" class="member-error">{{ memberState.error }}</p>
</template>
<template v-else>
<div class="member-action-group">
<button class="btn primary-action button-like" type="button" @click="doLogin">Sign In</button>
<RouterLink class="btn secondary" to="/request-access">Register</RouterLink>
</div>
<a v-if="auth.resetUrl" class="member-reset" :href="auth.resetUrl" target="_blank" rel="noopener noreferrer">
Trouble signing in? Reset password
</a>
</template>
</div>
<div class="member-service-row" aria-label="Member service access">
<div v-for="service in content.memberServices" :key="service.name" class="member-service-item">
<a
v-if="isExternal(service.href)"
class="member-service-link"
:href="service.href"
:aria-label="serviceTooltip(service)"
:title="serviceTooltip(service)"
target="_blank"
rel="noopener noreferrer"
>
<span class="member-service-icon" aria-hidden="true">{{ service.icon }}</span>
</a>
<RouterLink
v-else
class="member-service-link"
:to="service.href"
:aria-label="serviceTooltip(service)"
:title="serviceTooltip(service)"
>
<span class="member-service-icon" aria-hidden="true">{{ service.icon }}</span>
</RouterLink>
<span class="member-service-tooltip" aria-hidden="true">
<strong>{{ service.name }}</strong>
<span>{{ service.description }}</span>
</span>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { RouterLink } from "vue-router";
import { auth, login, logout } from "../auth";
defineProps({
content: Object,
memberPanelCopy: String,
memberAction: Object,
memberState: Object,
});
function isExternal(href) {
return typeof href === "string" && /^https?:\/\//.test(href);
}
function serviceTooltip(service) {
return `${service.name}: ${service.description}`;
}
function doLogin() {
login();
}
function doLogout() {
logout();
}
</script>
<style scoped>
.conversion-section {
box-sizing: border-box;
display: grid;
align-content: start;
scroll-margin-top: 0;
margin-top: 0;
padding: clamp(54px, 7vh, 82px) 0;
}
.conversion-card {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(300px, 0.7fr);
gap: 24px;
align-items: center;
padding: clamp(28px, 4vw, 42px);
}
.section-kicker {
color: var(--accent-cyan);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0 8px;
}
.conversion-card h2 {
margin: 0;
font-size: clamp(30px, 5vw, 46px);
letter-spacing: 0;
}
.conversion-card p {
font-size: 17px;
line-height: 1.65;
}
#member-access {
padding-top: clamp(40px, 5vh, 64px);
padding-bottom: clamp(34px, 5vh, 56px);
}
.member-panel {
grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
gap: 18px 28px;
align-items: center;
padding: clamp(22px, 3vw, 30px);
border-radius: 18px;
background:
linear-gradient(100deg, rgba(255, 255, 255, 0.045), rgba(5, 12, 28, 0.72)),
rgba(255, 255, 255, 0.025);
}
.member-panel h2 {
font-size: clamp(28px, 4vw, 42px);
}
.member-panel p {
max-width: 740px;
margin: 10px 0 0;
}
.member-copy {
min-width: 0;
}
.member-service-row {
position: relative;
grid-column: 1 / -1;
display: flex;
flex-wrap: nowrap;
justify-content: center;
gap: 7px;
width: 100%;
max-width: none;
margin-top: 0;
padding-top: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.member-service-item {
position: relative;
flex: 0 0 auto;
}
.member-service-link {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 8px;
border: 1px solid rgba(0, 229, 197, 0.22);
background:
linear-gradient(135deg, rgba(0, 229, 197, 0.08), rgba(120, 180, 255, 0.06)),
rgba(255, 255, 255, 0.035);
text-decoration: none;
transition:
border-color 160ms ease,
background 160ms ease,
transform 160ms ease;
}
.member-service-link:hover,
.member-service-link:focus-visible {
border-color: rgba(0, 229, 197, 0.58);
background:
linear-gradient(135deg, rgba(0, 229, 197, 0.16), rgba(120, 180, 255, 0.12)),
rgba(255, 255, 255, 0.055);
transform: translateY(-2px);
}
.member-service-icon {
font-size: 19px;
line-height: 1;
}
.member-service-tooltip {
position: absolute;
left: 50%;
bottom: calc(100% + 10px);
z-index: 30;
width: max-content;
max-width: min(250px, 74vw);
display: grid;
gap: 4px;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(0, 229, 197, 0.22);
background: rgba(4, 11, 24, 0.96);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34);
color: var(--text-primary);
font-size: 13px;
line-height: 1.35;
opacity: 0;
pointer-events: none;
transform: translate(-50%, 4px);
visibility: hidden;
transition:
opacity 140ms ease,
transform 140ms ease,
visibility 140ms ease;
}
.member-service-tooltip::after {
content: "";
position: absolute;
left: 50%;
top: 100%;
width: 9px;
height: 9px;
border-right: 1px solid rgba(0, 229, 197, 0.22);
border-bottom: 1px solid rgba(0, 229, 197, 0.22);
background: rgba(4, 11, 24, 0.96);
transform: translate(-50%, -5px) rotate(45deg);
}
.member-service-tooltip strong {
color: var(--text-strong);
}
.member-service-tooltip span {
color: var(--text-muted);
}
.member-service-item:hover .member-service-tooltip,
.member-service-item:focus-within .member-service-tooltip {
opacity: 1;
transform: translate(-50%, 0);
visibility: visible;
}
.member-state-copy {
color: var(--text-muted);
}
.member-actions {
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-direction: column;
flex-wrap: nowrap;
gap: 12px;
width: 100%;
max-width: 360px;
justify-self: end;
}
.member-action-group {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
align-items: center;
width: 100%;
}
.action-card {
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
background: rgba(5, 12, 28, 0.55);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.member-action-group .btn {
width: 100%;
justify-content: center;
text-align: center;
}
.primary-action {
background: linear-gradient(135deg, rgba(0, 229, 197, 0.9), rgba(120, 180, 255, 0.82));
color: #02141d;
}
.tertiary {
background: transparent;
color: var(--text-strong);
}
.button-like {
cursor: pointer;
font: inherit;
}
.member-loading {
min-height: 44px;
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--text-muted);
}
.skeleton {
width: 96px;
height: 18px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.05));
}
.member-reset,
.member-error {
width: 100%;
}
.member-reset {
color: var(--accent-cyan);
font-size: 15px;
text-align: center;
}
.member-service-link:focus-visible {
outline: 2px solid rgba(0, 229, 197, 0.75);
outline-offset: 3px;
}
@media (max-width: 1080px) {
.conversion-card,
.member-panel {
grid-template-columns: 1fr;
}
.conversion-section {
min-height: auto;
}
.member-actions {
justify-self: stretch;
max-width: none;
}
}
@media (max-width: 760px) {
.conversion-section {
margin-top: 0;
padding: 50px 0;
}
.member-service-row {
justify-content: flex-start;
gap: 5px;
overflow-x: auto;
padding-bottom: 6px;
scrollbar-width: thin;
}
.member-service-link {
width: 34px;
height: 34px;
}
.member-service-icon {
font-size: 17px;
}
}
</style>

View File

@ -0,0 +1,398 @@
<template>
<section id="capabilities" class="content-section" aria-labelledby="capabilities-title">
<div class="section-heading">
<p class="section-kicker mono">{{ content.capabilitySection.kicker }}</p>
<h2 id="capabilities-title">{{ content.capabilitySection.heading }}</h2>
<p>{{ content.capabilitySection.description }}</p>
</div>
<div class="card-grid">
<article v-for="capability in content.capabilities" :key="capability.title" class="info-card">
<div class="icon-badge mono" aria-hidden="true">{{ capability.icon }}</div>
<h3>{{ capability.title }}</h3>
<p>{{ capability.description }}</p>
</article>
</div>
</section>
<section id="services" class="content-section" aria-labelledby="services-title">
<div class="section-heading">
<p class="section-kicker mono">{{ content.serviceSection.kicker }}</p>
<h2 id="services-title">{{ content.serviceSection.heading }}</h2>
<p>{{ content.serviceSection.description }}</p>
</div>
<div class="service-list">
<article v-for="service in content.services" :key="service.title" class="service-card">
<h3>{{ service.title }}</h3>
<p><strong>Build path:</strong> {{ service.investigation }}</p>
<p><strong>Outcome:</strong> {{ service.outcome }}</p>
</article>
</div>
</section>
<section id="work" class="content-section" aria-labelledby="work-title">
<div class="section-heading">
<p class="section-kicker mono">{{ content.workSection.kicker }}</p>
<h2 id="work-title">{{ content.workSection.heading }}</h2>
<p>{{ content.workSection.description }}</p>
</div>
<div class="work-grid">
<article
v-for="item in content.selectedWork"
:key="item.title"
class="work-card card"
:class="{ minor: item.weight === 'minor' }"
>
<div class="work-card-top">
<div class="work-icon mono" aria-hidden="true">{{ item.icon }}</div>
<div>
<p class="work-kind mono">{{ item.kind }}</p>
<h3>{{ item.title }}</h3>
</div>
</div>
<p>{{ item.description }}</p>
<div v-if="item.points?.length || item.links?.length" class="work-proof">
<div v-if="item.points?.length" class="work-points" aria-label="Selected proof points">
<span v-for="point in item.points" :key="point" class="pill mono">{{ point }}</span>
</div>
<div v-if="item.links?.length" class="work-links">
<a
v-for="link in item.links"
:key="link.href"
class="work-link mono"
:href="link.href"
target="_blank"
rel="noopener noreferrer"
>
{{ link.label }}
</a>
</div>
</div>
</article>
</div>
</section>
<section id="engagement" class="content-section" aria-labelledby="engagement-title">
<div class="section-heading centered">
<p class="section-kicker mono">Engagement</p>
<h2 id="engagement-title">Ways to Work Together</h2>
<p>
Fixed-scope and contract pricing is based on the problem, access requirements, urgency, and expected deliverables.
</p>
</div>
<div class="engagement-grid">
<article v-for="item in content.engagementTypes" :key="item.title" class="info-card">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<div class="duration mono">{{ item.duration }}</div>
</article>
</div>
</section>
<section id="process" class="content-section" aria-labelledby="process-title">
<div class="section-heading">
<p class="section-kicker mono">Process</p>
<h2 id="process-title">A Practical Engagement Process</h2>
</div>
<ol class="process-list">
<li v-for="(step, index) in content.processSteps" :key="step.title" class="process-step">
<span class="step-number mono">{{ index + 1 }}</span>
<div>
<h3>{{ step.title }}</h3>
<p>{{ step.description }}</p>
</div>
</li>
</ol>
<p class="safety-note">
Do not email passwords, private keys, tokens, customer data, or other secrets. Secure access can be arranged after
scope is confirmed.
</p>
</section>
</template>
<script setup>
defineProps({
content: Object,
});
</script>
<style scoped>
.content-section {
box-sizing: border-box;
display: grid;
align-content: start;
scroll-margin-top: 0;
margin-top: 0;
padding: clamp(58px, 7vh, 88px) 0;
}
.section-kicker {
color: var(--accent-cyan);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0 8px;
}
.section-heading {
max-width: 820px;
margin-bottom: 22px;
}
.section-heading.centered {
width: min(100%, 920px);
margin-left: auto;
margin-right: auto;
}
.section-heading h2 {
margin: 0;
font-size: clamp(30px, 5vw, 46px);
letter-spacing: 0;
}
.section-heading p {
font-size: 17px;
line-height: 1.65;
}
.card-grid,
.work-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.work-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.engagement-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
width: min(100%, 920px);
margin: 0 auto;
}
.info-card,
.service-card {
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
padding: 18px;
background: rgba(255, 255, 255, 0.035);
}
.work-card {
position: relative;
overflow: hidden;
padding: 16px;
display: flex;
flex-direction: column;
}
.work-card::after {
content: "";
position: absolute;
inset: auto 0 0;
height: 3px;
background: linear-gradient(90deg, rgba(0, 229, 197, 0.7), rgba(120, 180, 255, 0.4), transparent);
opacity: 0.5;
}
.work-card.minor {
grid-column: 1 / -1;
display: grid;
grid-template-columns: minmax(0, 0.7fr) minmax(280px, 1fr) minmax(250px, 0.8fr);
gap: 14px 22px;
align-items: start;
}
.work-card-top {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 14px;
}
.work-icon {
width: 46px;
height: 46px;
flex: 0 0 auto;
display: grid;
place-items: center;
border-radius: 14px;
border: 1px solid rgba(0, 229, 197, 0.25);
color: var(--accent-cyan);
background:
linear-gradient(135deg, rgba(0, 229, 197, 0.1), rgba(120, 180, 255, 0.08)),
rgba(255, 255, 255, 0.035);
}
.work-kind {
margin: 0 0 4px;
color: var(--accent-cyan);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.info-card h3,
.service-card h3,
.work-card h3,
.process-step h3 {
margin: 0 0 8px;
letter-spacing: 0;
}
.info-card p,
.service-card p,
.work-card p,
.process-step p {
line-height: 1.6;
}
.work-card p {
color: var(--text-muted);
font-size: 15px;
}
.work-proof {
display: grid;
gap: 10px;
margin-top: auto;
padding-top: 12px;
}
.work-links,
.work-points {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 0;
}
.work-points {
gap: 7px;
}
.work-points .pill {
min-height: 30px;
padding: 5px 9px;
font-size: 12px;
}
.work-link {
min-height: 32px;
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 10px;
border: 1px solid rgba(0, 229, 197, 0.22);
color: var(--accent-cyan);
text-decoration: none;
background: rgba(0, 229, 197, 0.05);
font-size: 12px;
}
.work-card.minor .work-card-top {
margin-bottom: 0;
}
.work-card.minor .work-proof {
padding-top: 0;
}
.icon-badge {
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: 10px;
border: 1px solid rgba(0, 229, 197, 0.22);
color: var(--accent-cyan);
background: rgba(0, 229, 197, 0.06);
margin-bottom: 12px;
}
.service-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.service-card strong {
color: var(--text-strong);
}
.duration {
color: var(--accent-cyan);
margin-top: 12px;
font-size: 13px;
}
.process-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.process-step {
display: grid;
gap: 12px;
align-content: start;
padding: 18px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.035);
}
.step-number {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(0, 229, 197, 0.1);
border: 1px solid rgba(0, 229, 197, 0.22);
color: var(--accent-cyan);
}
.safety-note {
margin-top: 16px;
padding: 14px 16px;
border-left: 3px solid rgba(255, 220, 120, 0.45);
background: rgba(255, 220, 120, 0.05);
}
@media (max-width: 1080px) {
.card-grid,
.work-grid,
.service-list,
.engagement-grid,
.process-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.card-grid,
.engagement-grid,
.work-grid,
.service-list,
.process-list {
grid-template-columns: 1fr;
}
.content-section {
margin-top: 0;
padding: 50px 0;
}
.work-card.minor {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -103,6 +103,10 @@ function onWindowKeydown(event) {
menuButton.value?.focus();
}
/**
* WHY: Keep the opened mobile menu keyboard-contained and return Escape focus to the menu toggle.
* @param {KeyboardEvent} event Navigation key event from the primary nav.
*/
function onNavKeydown(event) {
if (event.key === "Escape") {
closeMenu();

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { RouterLinkStub, flushPromises, shallowMount } from "@vue/test-utils";
import { RouterLinkStub, flushPromises, mount, shallowMount } from "@vue/test-utils";
import PlatformSection from "../../../frontend/src/components/PlatformSection.vue";
import { auth } from "../../../frontend/src/auth.js";
@ -42,7 +42,7 @@ describe("HomeView", () => {
it("renders the professional homepage while logged out", () => {
resetAuth();
const wrapper = shallowMount(HomeView, {
const wrapper = mount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,
@ -106,7 +106,7 @@ describe("HomeView", () => {
it("shows a calm fallback when public status is unavailable", () => {
resetAuth();
const wrapper = shallowMount(HomeView, {
const wrapper = mount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,
@ -130,7 +130,7 @@ describe("HomeView", () => {
auth.username = "ada";
auth.email = "ada@example.dev";
const wrapper = shallowMount(HomeView, {
const wrapper = mount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,

View File

@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import { flushPromises, mount, shallowMount } from "@vue/test-utils";
import { nextTick } from "vue";
import axios from "axios";
@ -123,24 +124,56 @@ describe("static shell views and components", () => {
const logout = jest.spyOn(authModule, "logout").mockImplementation(() => {});
jest.spyOn(authModule, "login").mockImplementation(() => {});
const wrapper = mount(TopBar);
expect(wrapper.text()).toContain("Contact");
expect(wrapper.text()).toContain("Login");
expect(wrapper.text()).toContain("Register");
expect(wrapper.text()).not.toContain("Reset Password");
expect(wrapper.find("a[href='/#contact']").exists()).toBe(true);
const target = document.createElement("div");
document.body.appendChild(target);
const wrapper = mount(TopBar, { attachTo: target });
await wrapper.find("button.menu-toggle").trigger("click");
expect(wrapper.find("nav").classes()).toContain("open");
await wrapper.find("a[href='/#contact']").trigger("click");
expect(wrapper.find("nav").classes()).not.toContain("open");
try {
expect(wrapper.text()).toContain("Contact");
expect(wrapper.text()).toContain("Login");
expect(wrapper.text()).toContain("Register");
expect(wrapper.text()).not.toContain("Reset Password");
expect(wrapper.find("a[href='/#contact']").exists()).toBe(true);
auth.authenticated = true;
await flushPromises();
expect(wrapper.text()).toContain("Account");
expect(wrapper.text()).toContain("Open Services");
await wrapper.findAll("button.button").at(-1).trigger("click");
expect(logout).toHaveBeenCalled();
await wrapper.find("button.menu-toggle").trigger("click");
const nav = wrapper.find("nav");
expect(nav.classes()).toContain("open");
const navItems = () => Array.from(nav.element.querySelectorAll("a[href], button:not([disabled])"));
navItems()[0].focus();
nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true, cancelable: true }));
await nextTick();
expect(document.activeElement).toBe(navItems().at(-1));
navItems().at(-1).focus();
nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }));
await nextTick();
expect(document.activeElement).toBe(navItems()[0]);
nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
await nextTick();
expect(nav.classes()).not.toContain("open");
expect(document.activeElement).toBe(wrapper.find("button.menu-toggle").element);
await wrapper.find("button.menu-toggle").trigger("click");
expect(nav.classes()).toContain("open");
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
await nextTick();
expect(nav.classes()).not.toContain("open");
await wrapper.find("button.menu-toggle").trigger("click");
await wrapper.find("a[href='/#contact']").trigger("click");
expect(nav.classes()).not.toContain("open");
auth.authenticated = true;
await flushPromises();
expect(wrapper.text()).toContain("Account");
expect(wrapper.text()).toContain("Open Services");
await wrapper.findAll("button.button").at(-1).trigger("click");
expect(logout).toHaveBeenCalled();
} finally {
wrapper.unmount();
target.remove();
}
});
it("loads Monero status and handles API failures", async () => {