Extract glass system into shared CSS utilities and design tokens

Centralize all hardcoded rgba color values into CSS custom properties
and extract glass/glow styles into reusable utility classes (.glass,
.glass-inner, .glow-border, .glow-border--animated) in main.css.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 18:35:36 +01:00
parent 29974704d0
commit 019ead7be3
7 changed files with 133 additions and 55 deletions

View File

@@ -16,9 +16,26 @@
--color-text-on-gradient: #ffffff; --color-text-on-gradient: #ffffff;
--color-surface: #fff5f8; --color-surface: #fff5f8;
--color-card: #ffffff; --color-card: #ffffff;
--color-dark-base: #1B1730;
/* Glass system */
--color-glass: rgba(255, 255, 255, 0.1); --color-glass: rgba(255, 255, 255, 0.1);
--color-glass-strong: rgba(255, 255, 255, 0.15);
--color-glass-subtle: rgba(255, 255, 255, 0.05);
--color-glass-border: rgba(255, 255, 255, 0.18); --color-glass-border: rgba(255, 255, 255, 0.18);
--color-glass-border-hover: rgba(255, 255, 255, 0.3);
--color-glass-hover: rgba(255, 255, 255, 0.18); --color-glass-hover: rgba(255, 255, 255, 0.18);
--color-glass-inner: rgba(27, 23, 48, 0.55);
--color-glass-overlay: rgba(27, 23, 48, 0.4);
/* Text on gradient (opacity variants) */
--color-text-muted: rgba(255, 255, 255, 0.5);
--color-text-secondary: rgba(255, 255, 255, 0.7);
--color-text-soft: rgba(255, 255, 255, 0.85);
--color-text-bright: rgba(255, 255, 255, 0.9);
/* Glow border */
--gradient-glow: conic-gradient(from 135deg, var(--color-gradient-start), var(--color-gradient-mid), var(--color-gradient-end), var(--color-gradient-start));
/* Gradient */ /* Gradient */
--gradient-primary: linear-gradient(135deg, #f06292 0%, #ab47bc 50%, #5c6bc0 100%); --gradient-primary: linear-gradient(135deg, #f06292 0%, #ab47bc 50%, #5c6bc0 100%);
@@ -63,7 +80,7 @@ html {
body { body {
min-height: 100vh; min-height: 100vh;
background-color: #1B1730; background-color: var(--color-dark-base);
position: relative; position: relative;
} }
@@ -71,7 +88,7 @@ body::before {
content: ''; content: '';
position: fixed; position: fixed;
inset: 0; inset: 0;
background-color: #1B1730; background-color: var(--color-dark-base);
background-image: background-image:
radial-gradient(at 70% 20%, rgba(240, 98, 146, 0.55) 0px, transparent 50%), radial-gradient(at 70% 20%, rgba(240, 98, 146, 0.55) 0px, transparent 50%),
radial-gradient(at 25% 50%, rgba(171, 71, 188, 0.5) 0px, transparent 55%), radial-gradient(at 25% 50%, rgba(171, 71, 188, 0.5) 0px, transparent 55%),
@@ -194,6 +211,68 @@ textarea.form-field {
100% { background-position: -200% 0; } 100% { background-position: -200% 0; }
} }
/* ── Glass System ── */
/* Glass surface: passive containers on gradient (cards, icon boxes) */
.glass {
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
border: 1px solid var(--color-glass-border);
box-shadow: var(--shadow-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.glass:hover {
background: var(--color-glass-hover);
border-color: var(--color-glass-border-hover);
}
/* Glass interactive inner: dark translucent fill for interactive elements (FAB, CTA) */
.glass-inner {
background: var(--color-glass-inner);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
/* Glow border: conic gradient wrapper with halo (static) */
.glow-border {
background: var(--gradient-glow);
padding: 2px;
position: relative;
}
.glow-border::before {
content: '';
position: absolute;
inset: -4px;
border-radius: inherit;
background: var(--gradient-glow);
filter: blur(8px);
opacity: 0.3;
z-index: -1;
}
/* Glow border animated variant */
@property --glow-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.glow-border--animated {
background: conic-gradient(from var(--glow-angle), var(--color-gradient-start), var(--color-gradient-mid), var(--color-gradient-end), var(--color-gradient-start));
animation: glow-rotate 4s linear infinite;
}
.glow-border--animated::before {
background: conic-gradient(from var(--glow-angle), var(--color-gradient-start), var(--color-gradient-mid), var(--color-gradient-end), var(--color-gradient-start));
animation: glow-rotate 4s linear infinite;
}
@keyframes glow-rotate {
to { --glow-angle: 360deg; }
}
/* Utility */ /* Utility */
.text-center { .text-center {
text-align: center; text-align: center;

View File

@@ -28,7 +28,7 @@ defineProps<{
.attendee-list__heading { .attendee-list__heading {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
color: rgba(255, 255, 255, 0.5); color: var(--color-text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
@@ -44,7 +44,7 @@ defineProps<{
.attendee-list__item { .attendee-list__item {
font-size: 0.95rem; font-size: 0.95rem;
color: rgba(255, 255, 255, 0.85); color: var(--color-text-soft);
line-height: 1.4; line-height: 1.4;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -53,7 +53,7 @@ defineProps<{
.attendee-list__empty { .attendee-list__empty {
font-size: 0.9rem; font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5); color: var(--color-text-muted);
font-style: italic; font-style: italic;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<RouterLink to="/create" class="fab" aria-label="Create event"> <RouterLink to="/create" class="fab glow-border" aria-label="Create event">
<span class="fab__inner"> <span class="fab__inner glass-inner">
<span class="fab__icon" aria-hidden="true">+</span> <span class="fab__icon" aria-hidden="true">+</span>
</span> </span>
</RouterLink> </RouterLink>
@@ -18,37 +18,19 @@ import { RouterLink } from 'vue-router'
width: 56px; width: 56px;
height: 56px; height: 56px;
border-radius: 50%; border-radius: 50%;
background: conic-gradient(from 135deg, #F06292, #AB47BC, #5C6BC0, #F06292);
color: #fff; color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-decoration: none; text-decoration: none;
z-index: 100; z-index: 100;
padding: 2px;
transition: transform 0.15s ease; transition: transform 0.15s ease;
} }
.fab::before {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
background: conic-gradient(from 135deg, #F06292, #AB47BC, #5C6BC0, #F06292);
filter: blur(8px);
opacity: 0.3;
z-index: -1;
}
.fab__inner { .fab__inner {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%; border-radius: 50%;
background: rgba(27, 23, 48, 0.55);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -12,7 +12,7 @@ defineProps<{
.date-subheader { .date-subheader {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.85); color: var(--color-text-soft);
margin: 0; margin: 0;
padding: var(--spacing-xs) 0; padding: var(--spacing-xs) 0;
} }

View File

@@ -1,7 +1,9 @@
<template> <template>
<div class="empty-state"> <div class="empty-state">
<p class="empty-state__message">No events yet.<br />Create your first one!</p> <p class="empty-state__message">No events yet.<br />Create your first one!</p>
<RouterLink to="/create" class="btn-primary empty-state__cta">+ Create Event</RouterLink> <RouterLink to="/create" class="empty-state__cta glow-border glow-border--animated">
<span class="empty-state__cta-inner glass-inner">Create Event</span>
</RouterLink>
</div> </div>
</template> </template>
@@ -27,5 +29,34 @@ import { RouterLink } from 'vue-router'
.empty-state__cta { .empty-state__cta {
max-width: 280px; max-width: 280px;
width: 100%;
border-radius: var(--radius-button);
text-decoration: none;
transition: transform 0.1s ease;
}
.empty-state__cta-inner {
display: block;
width: 100%;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: calc(var(--radius-button) - 2px);
font-family: inherit;
font-size: 1rem;
font-weight: 700;
color: #fff;
text-align: center;
}
.empty-state__cta:hover {
transform: scale(1.02);
}
.empty-state__cta:active {
transform: scale(0.98);
}
.empty-state__cta:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="event-card" class="event-card glass"
:class="{ 'event-card--past': isPast, 'event-card--swiping': isSwiping }" :class="{ 'event-card--past': isPast, 'event-card--swiping': isSwiping }"
:style="swipeStyle" :style="swipeStyle"
@touchstart="onTouchStart" @touchstart="onTouchStart"
@@ -93,22 +93,12 @@ function onTouchEnd() {
.event-card { .event-card {
display: flex; display: flex;
align-items: center; align-items: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.05) 100%);
border: 1px solid var(--color-glass-border);
border-radius: var(--radius-card); border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
padding: var(--spacing-md) var(--spacing-lg); padding: var(--spacing-md) var(--spacing-lg);
gap: var(--spacing-sm); gap: var(--spacing-sm);
transition: background 0.2s ease, border-color 0.2s ease; transition: background 0.2s ease, border-color 0.2s ease;
} }
.event-card:hover {
background: var(--color-glass-hover);
border-color: rgba(255, 255, 255, 0.3);
}
.event-card--past { .event-card--past {
opacity: 0.6; opacity: 0.6;
filter: saturate(0.5); filter: saturate(0.5);
@@ -140,7 +130,7 @@ function onTouchEnd() {
.event-card__time { .event-card__time {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 400; font-weight: 400;
color: rgba(255, 255, 255, 0.7); color: var(--color-text-secondary);
} }
.event-card__badge { .event-card__badge {
@@ -158,8 +148,8 @@ function onTouchEnd() {
} }
.event-card__badge--attendee { .event-card__badge--attendee {
background: rgba(255, 255, 255, 0.15); background: var(--color-glass-strong);
color: rgba(255, 255, 255, 0.9); color: var(--color-text-bright);
} }
.event-card__delete { .event-card__delete {
@@ -172,7 +162,7 @@ function onTouchEnd() {
background: none; background: none;
border: none; border: none;
font-size: 1.2rem; font-size: 1.2rem;
color: rgba(255, 255, 255, 0.5); color: var(--color-text-muted);
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
transition: color 0.15s ease, background 0.15s ease; transition: color 0.15s ease, background 0.15s ease;

View File

@@ -33,21 +33,21 @@
<dl class="detail__meta"> <dl class="detail__meta">
<div class="detail__meta-item"> <div class="detail__meta-item">
<dt class="detail__meta-icon" aria-label="Date and time"> <dt class="detail__meta-icon glass" aria-label="Date and time">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</dt> </dt>
<dd class="detail__meta-text">{{ formattedDateTime }}</dd> <dd class="detail__meta-text">{{ formattedDateTime }}</dd>
</div> </div>
<div v-if="event.location" class="detail__meta-item"> <div v-if="event.location" class="detail__meta-item">
<dt class="detail__meta-icon" aria-label="Location"> <dt class="detail__meta-icon glass" aria-label="Location">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
</dt> </dt>
<dd class="detail__meta-text">{{ event.location }}</dd> <dd class="detail__meta-text">{{ event.location }}</dd>
</div> </div>
<div class="detail__meta-item"> <div class="detail__meta-item">
<dt class="detail__meta-icon" aria-label="Attendees"> <dt class="detail__meta-icon glass" aria-label="Attendees">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</dt> </dt>
<dd class="detail__meta-text">{{ event.attendeeCount }} going</dd> <dd class="detail__meta-text">{{ event.attendeeCount }} going</dd>
@@ -288,7 +288,7 @@ onMounted(fetchEvent)
inset: 0; inset: 0;
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
rgba(27, 23, 48, 0.4) 0%, var(--color-glass-overlay) 0%,
transparent 50% transparent 50%
); );
} }
@@ -369,11 +369,7 @@ onMounted(fetchEvent)
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.05) 100%);
border: 1px solid var(--color-glass-border);
border-radius: 10px; border-radius: 10px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
color: var(--color-text-on-gradient); color: var(--color-text-on-gradient);
} }
@@ -393,14 +389,14 @@ onMounted(fetchEvent)
.detail__section-title { .detail__section-title {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
color: rgba(255, 255, 255, 0.5); color: var(--color-text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
.detail__description { .detail__description {
font-size: 0.95rem; font-size: 0.95rem;
color: rgba(255, 255, 255, 0.85); color: var(--color-text-soft);
line-height: 1.6; line-height: 1.6;
word-break: break-word; word-break: break-word;
} }
@@ -415,8 +411,8 @@ onMounted(fetchEvent)
} }
.detail__banner--expired { .detail__banner--expired {
background: rgba(255, 255, 255, 0.12); background: var(--color-glass);
color: rgba(255, 255, 255, 0.8); color: var(--color-text-soft);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
@@ -429,7 +425,7 @@ onMounted(fetchEvent)
/* Skeleton shimmer on gradient */ /* Skeleton shimmer on gradient */
.skeleton { .skeleton {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.25) 50%, rgba(255, 255, 255, 0.1) 75%); background: linear-gradient(90deg, var(--color-glass) 25%, var(--color-glass-hover) 50%, var(--color-glass) 75%);
background-size: 200% 100%; background-size: 200% 100%;
} }