You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
761 lines
19 KiB
761 lines
19 KiB
<script setup lang="ts">
|
|
definePageMeta({
|
|
title: '项目',
|
|
})
|
|
|
|
const { loggedIn, initialized } = useAuthSession()
|
|
const { $toast } = useNuxtApp()
|
|
|
|
// ── State ──
|
|
const projects = ref<any[]>([])
|
|
const loading = ref(true)
|
|
const showForm = ref(false)
|
|
const editingProject = ref<any>(null)
|
|
const formData = ref({ name: '', tags: '', description: '', path: '' })
|
|
const submitting = ref(false)
|
|
|
|
// ── Intersection-observer entrance ──
|
|
const gridEl = ref<HTMLElement | null>(null)
|
|
|
|
// After cards render, add visible class with stagger
|
|
function revealCards() {
|
|
if (!gridEl.value) return
|
|
for (let i = 0; i < gridEl.value.children.length; i++) {
|
|
const child = gridEl.value.children[i] as HTMLElement
|
|
if (child) {
|
|
child.style.setProperty('--idx', String(i))
|
|
child.classList.add('visible')
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Fetch ──
|
|
async function fetchProjects() {
|
|
loading.value = true
|
|
try {
|
|
const res = await $fetch('/api/projects')
|
|
projects.value = (res as any)?.data?.items ?? []
|
|
await nextTick()
|
|
revealCards()
|
|
} catch (e: any) {
|
|
$toast?.error('加载项目失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// ── CRUD ──
|
|
function openCreate() {
|
|
editingProject.value = null
|
|
formData.value = { name: '', tags: '', description: '', path: '' }
|
|
showForm.value = true
|
|
}
|
|
|
|
function openEdit(project: any) {
|
|
editingProject.value = project
|
|
formData.value = {
|
|
name: project.name,
|
|
tags: project.tags || '',
|
|
description: project.description || '',
|
|
path: project.path || '',
|
|
}
|
|
showForm.value = true
|
|
}
|
|
|
|
async function submitForm() {
|
|
submitting.value = true
|
|
try {
|
|
const payload = {
|
|
name: formData.value.name,
|
|
tags: formData.value.tags || null,
|
|
description: formData.value.description || null,
|
|
path: formData.value.path || null,
|
|
}
|
|
if (editingProject.value) {
|
|
await $fetch(`/api/projects/${editingProject.value.id}`, { method: 'PUT', body: payload })
|
|
$toast?.success('项目已更新')
|
|
} else {
|
|
await $fetch('/api/projects', { method: 'POST', body: payload })
|
|
$toast?.success('项目已创建')
|
|
}
|
|
showForm.value = false
|
|
await fetchProjects()
|
|
} catch (e: any) {
|
|
$toast?.error(e?.data?.statusMessage || '操作失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
async function deleteProject(project: any) {
|
|
if (!confirm(`确定删除「${project.name}」?`)) return
|
|
try {
|
|
await $fetch(`/api/projects/${project.id}`, { method: 'DELETE' })
|
|
$toast?.success('项目已删除')
|
|
await fetchProjects()
|
|
} catch (e: any) {
|
|
$toast?.error('删除失败')
|
|
}
|
|
}
|
|
|
|
function parseTags(tags: string | null): string[] {
|
|
if (!tags) return []
|
|
return tags.split(',').map(t => t.trim()).filter(Boolean)
|
|
}
|
|
|
|
const cardBgPool = [
|
|
'var(--color-surface-card, #efe9de)',
|
|
'var(--color-surface-soft, #f5f0e8)',
|
|
'var(--color-canvas, #faf9f5)',
|
|
]
|
|
|
|
function cardBg(index: number): string {
|
|
return cardBgPool[index % cardBgPool.length]
|
|
}
|
|
|
|
function padNum(i: number): string {
|
|
return String(i + 1).padStart(2, '0')
|
|
}
|
|
|
|
onMounted(() => { fetchProjects() })
|
|
</script>
|
|
|
|
<template>
|
|
<div class="projects-page">
|
|
<!-- Header -->
|
|
<header class="page-header">
|
|
<div class="header-content">
|
|
<p class="page-eyebrow">Portfolio</p>
|
|
<h1 class="page-title">项目</h1>
|
|
<p class="page-subtitle">一些有趣的项目与探索</p>
|
|
</div>
|
|
<button v-if="loggedIn && initialized" class="add-btn" @click="openCreate">
|
|
<span class="add-icon">+</span>
|
|
新建项目
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="loading-state">
|
|
<span class="loader-dot" />
|
|
<span class="loader-dot" />
|
|
<span class="loader-dot" />
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else-if="projects.length === 0" class="empty-state">
|
|
<div class="empty-mark">✦</div>
|
|
<p class="empty-text">还没有项目</p>
|
|
<p class="empty-hint">项目展示从这里开始</p>
|
|
<button v-if="loggedIn && initialized" class="add-btn" @click="openCreate">
|
|
创建第一个项目
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Card Grid -->
|
|
<div v-else ref="gridEl" class="project-grid">
|
|
<article
|
|
v-for="(project, index) in projects"
|
|
:key="project.id"
|
|
:data-index="index"
|
|
class="project-card"
|
|
:class="{ 'card-featured': index === 0 && projects.length < 4 }"
|
|
:style="{ '--card-bg': cardBg(index), '--idx': index }"
|
|
>
|
|
<!-- Decorative index number -->
|
|
<span class="card-index">{{ padNum(index) }}</span>
|
|
|
|
<div class="card-body">
|
|
<h3 class="card-title">{{ project.name }}</h3>
|
|
|
|
<div v-if="project.tags" class="card-tags">
|
|
<span
|
|
v-for="tag in parseTags(project.tags)"
|
|
:key="tag"
|
|
class="tag-pill"
|
|
>{{ tag }}</span>
|
|
</div>
|
|
|
|
<p v-if="project.description" class="card-desc">{{ project.description }}</p>
|
|
|
|
<a
|
|
v-if="project.path"
|
|
:href="project.path"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="card-link"
|
|
@click.stop
|
|
>
|
|
<svg class="link-svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
<polyline points="15 3 21 3 21 9"/>
|
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
</svg>
|
|
<span class="link-label">查看项目</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div v-if="loggedIn && initialized" class="card-actions">
|
|
<button class="action-btn" title="编辑" @click="openEdit(project)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="action-btn action-delete" title="删除" @click="deleteProject(project)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<!-- Form Modal -->
|
|
<Teleport to="body">
|
|
<div v-if="showForm" class="modal-overlay" @click.self="showForm = false">
|
|
<div class="modal-card">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">{{ editingProject ? '编辑项目' : '新建项目' }}</h2>
|
|
<button class="modal-close" @click="showForm = false">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<form class="modal-body" @submit.prevent="submitForm">
|
|
<div class="form-group">
|
|
<label class="form-label">项目名 <span class="required">*</span></label>
|
|
<input v-model="formData.name" class="form-input" placeholder="输入项目名称" required maxlength="100" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">标签</label>
|
|
<input v-model="formData.tags" class="form-input" placeholder="用逗号分隔,如: Vue, Nuxt, 项目" maxlength="500" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">描述</label>
|
|
<textarea v-model="formData.description" class="form-textarea" placeholder="简短的描述" rows="3" maxlength="1000" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">链接</label>
|
|
<input v-model="formData.path" class="form-input" placeholder="https://github.com/..." maxlength="500" />
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn-cancel" @click="showForm = false">取消</button>
|
|
<button type="submit" class="btn-submit" :disabled="submitting">
|
|
{{ submitting ? '保存中...' : (editingProject ? '更新' : '创建') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ═══════════════════════
|
|
Projects Page — Brand
|
|
═══════════════════════ */
|
|
|
|
.projects-page {
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 32px 32px 80px;
|
|
}
|
|
|
|
/* ─── Header ─── */
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
margin-bottom: 36px;
|
|
gap: 24px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.header-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.page-eyebrow {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--color-primary, #cc785c);
|
|
margin: 0 0 8px;
|
|
}
|
|
|
|
.page-title {
|
|
font-family: 'Copernicus', 'Times New Roman', serif;
|
|
font-size: 36px;
|
|
font-weight: 400;
|
|
color: var(--color-ink, #141413);
|
|
letter-spacing: -0.8px;
|
|
margin: 0 0 6px;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.page-subtitle {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 16px;
|
|
color: var(--color-muted, #6c6a64);
|
|
margin: 0;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
/* ─── Add Button ─── */
|
|
|
|
.add-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-on-primary, #ffffff);
|
|
background: var(--color-primary, #cc785c);
|
|
border: none;
|
|
padding: 11px 22px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease, transform 0.15s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.add-btn:hover {
|
|
background: var(--color-primary-active, #a9583e);
|
|
}
|
|
|
|
.add-btn:active {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
.add-icon {
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* ─── Loading ─── */
|
|
|
|
.loading-state {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 120px 0;
|
|
}
|
|
|
|
.loader-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--color-primary, #cc785c);
|
|
animation: dotPulse 1.2s ease-in-out infinite;
|
|
}
|
|
|
|
.loader-dot:nth-child(2) { animation-delay: 0.15s; }
|
|
.loader-dot:nth-child(3) { animation-delay: 0.3s; }
|
|
|
|
@keyframes dotPulse {
|
|
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
|
|
40% { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
/* ─── Empty State ─── */
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 120px 0;
|
|
gap: 12px;
|
|
}
|
|
|
|
.empty-mark {
|
|
font-size: 36px;
|
|
color: var(--color-primary, #cc785c);
|
|
margin-bottom: 8px;
|
|
animation: gentleSpin 4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes gentleSpin {
|
|
0%, 100% { transform: rotate(0deg); }
|
|
50% { transform: rotate(180deg); }
|
|
}
|
|
|
|
.empty-text {
|
|
font-family: 'Copernicus', 'Times New Roman', serif;
|
|
font-size: 24px;
|
|
font-weight: 400;
|
|
color: var(--color-ink, #141413);
|
|
margin: 0;
|
|
}
|
|
|
|
.empty-hint {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 14px;
|
|
color: var(--color-muted, #6c6a64);
|
|
margin: 0 0 8px;
|
|
}
|
|
|
|
/* ═══════════════════════
|
|
Project Grid
|
|
═══════════════════════ */
|
|
|
|
.project-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
/* Featured card: first card spans wider when few projects */
|
|
.project-grid:has(:only-child) .card-featured,
|
|
.project-grid:has(:nth-child(2):last-child) .card-featured {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
/* ─── Card ─── */
|
|
|
|
.project-card {
|
|
position: relative;
|
|
background: var(--card-bg, var(--color-surface-card, #efe9de));
|
|
border-radius: 14px;
|
|
padding: 24px 24px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
opacity: 1;
|
|
transform: translateY(32px) rotateX(4deg);
|
|
transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.35s ease;
|
|
transform-origin: center top;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
.project-card.visible {
|
|
opacity: 1;
|
|
transform: translateY(0) rotateX(0deg);
|
|
transition-delay: calc(var(--idx, 0) * 0.08s);
|
|
}
|
|
|
|
.project-card:hover {
|
|
transform: translateY(-4px) scale(1.005);
|
|
box-shadow: 0 12px 36px rgba(20, 20, 19, 0.07);
|
|
}
|
|
|
|
/* ─── Card Index ─── */
|
|
|
|
.card-index {
|
|
font-family: 'Copernicus', 'Times New Roman', serif;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
color: var(--color-primary, #cc785c);
|
|
letter-spacing: 1px;
|
|
line-height: 1;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* ─── Card Body ─── */
|
|
|
|
.card-body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.card-title {
|
|
font-family: 'Copernicus', 'Times New Roman', serif;
|
|
font-size: 22px;
|
|
font-weight: 400;
|
|
color: var(--color-ink, #141413);
|
|
margin: 0;
|
|
letter-spacing: -0.3px;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
/* ─── Tags ─── */
|
|
|
|
.card-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
}
|
|
|
|
.tag-pill {
|
|
display: inline-block;
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--color-primary, #cc785c);
|
|
background: rgba(204, 120, 92, 0.1);
|
|
padding: 3px 9px;
|
|
border-radius: 9999px;
|
|
line-height: 1.4;
|
|
letter-spacing: 0.2px;
|
|
}
|
|
|
|
/* ─── Description ─── */
|
|
|
|
.card-desc {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 14px;
|
|
color: var(--color-body, #3d3d3a);
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
}
|
|
|
|
/* ─── Link ─── */
|
|
|
|
.card-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--color-primary, #cc785c);
|
|
text-decoration: none;
|
|
transition: gap 0.2s ease;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.card-link:hover {
|
|
gap: 10px;
|
|
}
|
|
|
|
.link-svg {
|
|
flex-shrink: 0;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.card-link:hover .link-svg {
|
|
transform: translate(2px, -2px);
|
|
}
|
|
|
|
/* ─── Card Actions ─── */
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
justify-content: flex-end;
|
|
border-top: 1px solid rgba(230, 223, 216, 0.4);
|
|
padding-top: 14px;
|
|
}
|
|
|
|
.action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 30px;
|
|
height: 30px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
color: var(--color-muted, #6c6a64);
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: var(--color-canvas, #faf9f5);
|
|
color: var(--color-ink, #141413);
|
|
}
|
|
|
|
.action-delete:hover {
|
|
background: rgba(198, 69, 69, 0.08);
|
|
color: var(--color-error, #c64545);
|
|
}
|
|
|
|
/* ═══════════════════════
|
|
Modal
|
|
═══════════════════════ */
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 200;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(20, 20, 19, 0.45);
|
|
backdrop-filter: blur(5px);
|
|
padding: 24px;
|
|
}
|
|
|
|
.modal-card {
|
|
background: var(--color-canvas, #faf9f5);
|
|
border-radius: 16px;
|
|
width: 100%;
|
|
max-width: 480px;
|
|
box-shadow: 0 20px 60px rgba(20, 20, 19, 0.18);
|
|
animation: modalIn 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
@keyframes modalIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.93) translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 24px 24px 0;
|
|
}
|
|
|
|
.modal-title {
|
|
font-family: 'Copernicus', 'Times New Roman', serif;
|
|
font-size: 22px;
|
|
font-weight: 400;
|
|
color: var(--color-ink, #141413);
|
|
margin: 0;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
|
|
.modal-close {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
color: var(--color-muted, #6c6a64);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
background: var(--color-surface-card, #efe9de);
|
|
color: var(--color-ink, #141413);
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 20px 24px 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.form-label {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--color-ink, #141413);
|
|
}
|
|
|
|
.required { color: var(--color-error, #c64545); }
|
|
|
|
.form-input,
|
|
.form-textarea {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 14px;
|
|
color: var(--color-ink, #141413);
|
|
background: var(--color-canvas, #faf9f5);
|
|
border: 1px solid var(--color-hairline, #e6dfd8);
|
|
border-radius: 8px;
|
|
padding: 10px 14px;
|
|
outline: none;
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.form-input:focus,
|
|
.form-textarea:focus {
|
|
border-color: var(--color-primary, #cc785c);
|
|
box-shadow: 0 0 0 3px rgba(204, 120, 92, 0.12);
|
|
}
|
|
|
|
.form-textarea {
|
|
resize: vertical;
|
|
min-height: 72px;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.btn-cancel {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-muted, #6c6a64);
|
|
background: transparent;
|
|
border: 1px solid var(--color-hairline, #e6dfd8);
|
|
padding: 9px 18px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-cancel:hover {
|
|
background: var(--color-surface-card, #efe9de);
|
|
color: var(--color-ink, #141413);
|
|
}
|
|
|
|
.btn-submit {
|
|
font-family: var(--font-body, 'Inter', sans-serif);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-on-primary, #ffffff);
|
|
background: var(--color-primary, #cc785c);
|
|
border: none;
|
|
padding: 9px 22px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.btn-submit:hover { background: var(--color-primary-active, #a9583e); }
|
|
.btn-submit:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
|
|
/* ═══════════════════════
|
|
Responsive
|
|
═══════════════════════ */
|
|
|
|
@media (max-width: 768px) {
|
|
.projects-page {
|
|
padding: 40px 20px 80px;
|
|
}
|
|
|
|
.page-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 32px;
|
|
}
|
|
|
|
.project-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 16px;
|
|
}
|
|
|
|
.project-card {
|
|
padding: 24px 24px 20px;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
</style>
|
|
|