diff --git a/.env.example b/.env.example
index d6093a3..225220a 100644
--- a/.env.example
+++ b/.env.example
@@ -9,7 +9,7 @@ RENTMAN_API_TOKEN=
JWT_SECRET=
# --- Database ---
-DATABASE_URL=postgresql://hms:hms@postgres:5432/hms
+DATABASE_URL=postgresql+asyncpg://hms:hms@postgres:5432/hms
# --- Redis ---
REDIS_URL=redis://redis:6379/0
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 7283440..75490a5 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -4,7 +4,7 @@ sqlalchemy[asyncio]>=2.0.30
asyncpg>=0.29.0
aiosqlite>=0.20.0
redis>=5.0.0
-pydantic>=2.7.0
+pydantic[email]>=2.7.0
pydantic-settings>=2.3.0
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
diff --git a/docker-compose.yml b/docker-compose.yml
index ba7065a..967bcb4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,7 +28,7 @@ services:
redis:
condition: service_healthy
environment:
- - DATABASE_URL=${DATABASE_URL:-postgresql://hms:hms@postgres:5432/hms}
+ - DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://hms:hms@postgres:5432/hms}
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
- RENTMAN_API_TOKEN=${RENTMAN_API_TOKEN}
- JWT_SECRET=${JWT_SECRET}
@@ -42,7 +42,7 @@ services:
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
restart: unless-stopped
healthcheck:
- test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
interval: 30s
timeout: 10s
retries: 3
diff --git a/docs/prototype-app.js b/docs/prototype-app.js
new file mode 100644
index 0000000..81eb8e7
--- /dev/null
+++ b/docs/prototype-app.js
@@ -0,0 +1,761 @@
+/* ============================================
+ HMS Licht & Ton – Vue 3 SPA Prototype v4
+ Dark Theme + Subtle Orange + Images + Speaker Grid
+ ============================================ */
+
+const { createApp, ref, reactive, computed, onMounted, defineComponent, h } = Vue;
+
+// ===== Logo Component =====
+const HmsLogo = defineComponent({
+ name: 'HmsLogo',
+ props: { size: { type: Number, default: 40 } },
+ setup(props) {
+ return () => h('svg', {
+ class: 'hms-logo-svg', viewBox: '0 0 200 200',
+ style: `height:${props.size}px;width:auto`,
+ 'aria-label': 'HMS Licht & Ton Logo', role: 'img'
+ }, [
+ h('path', { fill: 'currentColor', d: 'M166.266,172.872h-37.687v-60.718H74.226v60.718H36.539V26.956h37.687v56.335h54.354V26.956h37.687V172.872z' }),
+ h('rect', { x: '23.598', y: '15.343', fill: 'none', stroke: '#EC6925', 'stroke-width': '15.1525', 'stroke-miterlimit': '10', width: '155.612', height: '168.874' })
+ ]);
+ }
+});
+
+// ===== Speaker SVG Icon Component =====
+const SpeakerIcon = defineComponent({
+ name: 'SpeakerIcon',
+ props: { type: { type: String, default: 'linearray' } },
+ setup(props) {
+ const icons = {
+ linearray: ' ',
+ subwoofer: ' ',
+ monitor: ' ',
+ pointsource: ' ',
+ column: ' ',
+ movinghead: ' '
+ };
+ const svgStr = computed(() => '' + (icons[props.type] || icons.linearray) + ' ');
+ return { svgStr };
+ },
+ template: ' '
+});
+
+// ===== Header =====
+const AppHeader = defineComponent({
+ name: 'AppHeader',
+ setup() {
+ const mobileMenuOpen = ref(false);
+ const currentRoute = ref(window.location.hash || '#/');
+ const navItems = [
+ { label: 'Home', route: '#/' },
+ { label: 'Referenzen', route: '#/referenzen' },
+ { label: 'Mietkatalog', route: '#/mietkatalog' },
+ { label: 'Kontakt', route: '#/kontakt' }
+ ];
+ function navigate(route) { window.location.hash = route; mobileMenuOpen.value = false; window.scrollTo(0, 0); }
+ function isActive(route) { return currentRoute.value === route || (route !== '#/' && currentRoute.value.startsWith(route)); }
+ window.addEventListener('hashchange', () => { currentRoute.value = window.location.hash || '#/'; mobileMenuOpen.value = false; });
+ return { mobileMenuOpen, currentRoute, navItems, navigate, isActive };
+ },
+ template: `
+
+ `
+});
+
+// ===== Footer =====
+const AppFooter = defineComponent({
+ name: 'AppFooter',
+ setup() {
+ function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
+ const year = new Date().getFullYear();
+ return { navigate, year };
+ },
+ template: `
+
+ `
+});
+
+// ===== Service Card =====
+const ServiceCard = defineComponent({
+ name: 'ServiceCard',
+ props: { icon: String, title: String, description: String },
+ template: `
+
+
{{ icon }}
+
{{ title }}
+
{{ description }}
+
`
+});
+
+// ===== Equipment Card =====
+const EquipmentCard = defineComponent({
+ name: 'EquipmentCard',
+ props: { item: Object },
+ emits: ['click', 'add-to-cart'],
+ template: `
+
+
+
+
📦
+
{{ item.category }}
+
+
+
{{ item.name }}
+
{{ item.description }}
+
+ {{ item.code }}
+ + Hinzufügen
+
+
+
`
+});
+
+// ===== UX State Components =====
+const LoadingSkeleton = defineComponent({
+ name: 'LoadingSkeleton',
+ props: { count: { type: Number, default: 6 } },
+ template: `
+ `
+});
+
+const EmptyState = defineComponent({
+ name: 'EmptyState',
+ props: { icon: { type: String, default: '🔍' }, title: String, message: String, actionLabel: String },
+ emits: ['action'],
+ template: `
+
+
{{ icon }}
+
{{ title }}
+
{{ message }}
+
{{ actionLabel }}
+
`
+});
+
+const ErrorState = defineComponent({
+ name: 'ErrorState',
+ props: { title: { type: String, default: 'Ein Fehler ist aufgetreten' }, message: String },
+ emits: ['retry'],
+ template: `
+
+
+
{{ title }}
+
{{ message }}
+
Erneut versuchen
+
`
+});
+
+// ===== HOME PAGE with Hero Image + Speaker Grid =====
+const HomePage = defineComponent({
+ name: 'HomePage',
+ emits: ['navigate', 'add-to-cart'],
+ setup(_, { emit }) {
+ function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
+ const services = [
+ { icon: '🔊', title: 'Vermietung', description: 'Lautsprecher, Mischpulte, Lichtanlagen, Rigging und Komplett-Setups aus unserem 1.000+ Geräte umfassenden Mietlager in Ellzee. Geprüftes Equipment, sofort einsatzbereit.' },
+ { icon: '🛒', title: 'Verkauf', description: 'Neu- und Gebrauchtgeräte führender Hersteller mit Beratung, Inbetriebnahme und Service. Wir liefern nicht nur – wir konfigurieren Ihre Anlage einsatzfertig.' },
+ { icon: '👷', title: 'Personal', description: 'Qualifizierte Tontechniker, Lichttechniker und Rigger für Aufbau, Betrieb und Abbau. Mit TÜV-geprüfter Ausbildung und umfangreicher Praxiserfahrung.' },
+ { icon: '🚚', title: 'Transport', description: 'Eigene Transportfahrzeuge für sichere An- und Ablieferung. Inklusive Verladekonzept, Positionierung und Rückholung – kundengerecht terminiert.' },
+ { icon: '📦', title: 'Lagerung', description: 'Klimatisierte und trockene Lagerung für Equipment und Veranstaltungsbestände. Mit Bestandsmanagement und regelmäßiger Funktionsprüfung.' },
+ { icon: '🔧', title: 'Werkstatt', description: 'Herstellerübergreifende Reparatur und Wartung in unserer eigenen Werkstatt. Regelmäßige DGUV-Prüfungen und Kalibrierung für Ihren sicheren Betrieb.' },
+ { icon: '⚡', title: 'Installation', description: 'Festinstallationen in Schaufenstern, Lokalen und Veranstaltungsstätten. Von der Planung über die Elektroinstallation bis zur Abnahme – alles aus einer Hand.' },
+ { icon: '📅', title: 'Booking', description: 'Vermittlung von Künstlern und Technik-Personal für Ihre Veranstaltung. Mit unserem branchenweiten Netzwerk finden wir die passenden Professionals für Ihr Event.' }
+ ];
+ const speakers = [
+ { type: 'linearray', name: 'L-Acoustics K2', category: 'Line Array' },
+ { type: 'subwoofer', name: 'L-Acoustics KS28', category: 'Subwoofer' },
+ { type: 'pointsource', name: 'd&b T10', category: 'Point Source' },
+ { type: 'column', name: 'd&b KSL8', category: 'Column Array' },
+ { type: 'monitor', name: 'd&b M4', category: 'Monitor' },
+ { type: 'movinghead', name: 'Robe Pointe', category: 'Moving Head' }
+ ];
+ return { services, speakers, navigate };
+ },
+ template: `
+
+
+
+
+
+
+
+
Seit 2003 · Über 20 Jahre Veranstaltungstechnik
+
+ Professionelle Technikfür Ihre Veranstaltung
+
+
+ Von der Firmenevent-Beschallung bis zur Open-Air-Großproduktion: HMS Licht & Ton liefert Tontechnik, Lichttechnik, Rigging und Komplettlösungen aus Leipheim und Ellzee.
+
+
+
+ Mietkatalog öffnen
+
+
+
Beratung anfragen
+
+
+
+
+
+
+
+
+
+ Equipment-Highlights
+
Lautsprecher & Strahler aus unserem Mietlager
+
+
+
+
+
{{ sp.name }}
+
{{ sp.category }}
+
+
+
+
+
+
+
+
+
+
+
Unternehmen
+
Ihr Technik-Partner mit Erfahrung
+
Die Hammerschmidt u. Mössle GbR ist seit 2003 Ihr zuverlässiger Partner für Veranstaltungstechnik in der Region Leipheim / Ellzee und im gesamten süddeutschen Raum.
+
Mit einem Mietlager von über 1.000 Geräten, einer eigenen Werkstatt und erfahrenem Personal realisieren wir Veranstaltungen jeder Größenordnung.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Leistungen
+
Von der Vermietung bis zur Komplettbetreuung
+
+
+
+
+
+
+
+
+
+
+
+
Online-Mietkatalog
+
Durchsuchen Sie unser Equipment-Sortiment und stellen Sie Ihre Mietanfrage direkt online zusammen.
+
+ Katalog durchsuchen
+
+
+
+
+
+
`
+});
+
+// ===== REFERENZEN PAGE with real HMS images =====
+const ReferenzenPage = defineComponent({
+ name: 'ReferenzenPage',
+ setup() {
+ const loading = ref(true);
+ const error = ref(false);
+ const filter = ref('alle');
+ const categories = ['alle', 'Open-Air', 'Indoor', 'Rigging', 'Event'];
+ const allImages = [
+ { id: 1, title: 'Open-Air Veranstaltung', category: 'Open-Air', date: '2017', img: 'img/ref1.jpg' },
+ { id: 2, title: 'Bühnenaufbau', category: 'Rigging', date: '2016', img: 'img/ref2.jpg' },
+ { id: 3, title: 'Donautal Radelspass', category: 'Event', date: '2019', img: 'img/ref3.jpg' },
+ { id: 4, title: 'Traversenaufbau', category: 'Rigging', date: '2019', img: 'img/ref4.jpg' },
+ { id: 5, title: 'Veranstaltungstechnik vor Ort', category: 'Event', date: '2019', img: 'img/ref5.jpg' },
+ { id: 6, title: 'Indoor Beschallung', category: 'Indoor', date: '2016', img: 'img/ref6.jpg' },
+ { id: 7, title: 'Bühnentechnik Setup', category: 'Rigging', date: '2019', img: 'img/ref7.jpg' },
+ { id: 8, title: 'Event-Aufbau', category: 'Event', date: '2019', img: 'img/ref8.jpg' },
+ { id: 9, title: 'Großbühne Aufbau', category: 'Open-Air', date: '2016', img: 'img/ref9.jpg' }
+ ];
+ const filteredImages = computed(() => {
+ if (filter.value === 'alle') return allImages;
+ return allImages.filter(i => i.category === filter.value);
+ });
+ onMounted(() => { setTimeout(() => { loading.value = false; }, 1000); });
+ function retry() { loading.value = true; error.value = false; setTimeout(() => { loading.value = false; }, 800); }
+ return { loading, error, filter, categories, filteredImages, retry };
+ },
+ template: `
+
+
Referenzen
+
Ausgewählte Projekte aus unserer Veranstaltungstechnik-Praxis.
+
+ {{ cat === 'alle' ? 'Alle' : cat }}
+
+
+
+
+
+
+
+
+
{{ img.category }}
+
+ {{ img.title }} {{ img.date }}
+
+
+
+
`
+});
+
+// ===== KONTAKT PAGE =====
+const KontaktPage = defineComponent({
+ name: 'KontaktPage',
+ setup() {
+ const form = reactive({ name: '', email: '', phone: '', subject: '', message: '', privacy: false });
+ const errors = reactive({});
+ const submitted = ref(false);
+ const submitting = ref(false);
+ function validate() {
+ const e = {};
+ if (!form.name.trim()) e.name = 'Name ist erforderlich';
+ if (!form.email.trim()) e.email = 'E-Mail ist erforderlich';
+ else if (!/^\S+@\S+\.\S+$/.test(form.email)) e.email = 'Ungültige E-Mail-Adresse';
+ if (!form.message.trim()) e.message = 'Nachricht ist erforderlich';
+ if (!form.privacy) e.privacy = 'Bitte stimmen Sie der Datenschutzerklärung zu';
+ Object.keys(errors).forEach(k => delete errors[k]); Object.assign(errors, e);
+ return Object.keys(e).length === 0;
+ }
+ function submit() { if (!validate()) return; submitting.value = true; setTimeout(() => { submitting.value = false; submitted.value = true; }, 1500); }
+ function resetForm() { Object.assign(form, { name: '', email: '', phone: '', subject: '', message: '', privacy: false }); submitted.value = false; }
+ return { form, errors, submitted, submitting, validate, submit, resetForm };
+ },
+ template: `
+
+
Kontakt
+
Wir beraten Sie persönlich – telefonisch, per E-Mail oder über das Kontaktformular.
+
+
+
+
+
🕐 ÖffnungszeitenMontag – Freitag 10:00 – 18:00
Samstag & Sonntag Geschlossen
+
+
+
+
+
Nachricht senden
+
+
+
Nachricht übermittelt
+
Vielen Dank für Ihre Anfrage. Wir melden uns innerhalb von 24 Stunden bei Ihnen.
+
Neue Nachricht
+
+
+
+
+
+
`
+});
+
+// ===== MIETKATALOG PAGE =====
+const MietkatalogPage = defineComponent({
+ name: 'MietkatalogPage',
+ emits: ['navigate', 'add-to-cart'],
+ setup(_, { emit }) {
+ const loading = ref(true); const error = ref(false);
+ const searchQuery = ref(''); const activeCategory = ref('alle'); const sortBy = ref('name');
+ const categories = ['alle', 'Tontechnik', 'Lichttechnik', 'Rigging', 'Video', 'Strom', 'Zubehör'];
+ const allEquipment = [
+ { id: 1, code: 'LT-001', name: 'L-Acoustics K2', category: 'Tontechnik', description: 'High-Performance Line-Array Element mit variabler Krümmung für große Open-Air-Produktionen und Hallenbeschallung', brand: 'L-Acoustics', specs: { weight: '56 kg', power: '1440 W', freq_range: '35 Hz - 20 kHz' }, image: '' },
+ { id: 2, code: 'LT-002', name: 'd&b audiotechnik KSL8', category: 'Tontechnik', description: 'Bi-amplified Column Array Loudspeaker, 80° horizontal, für mittlere bis große Events', brand: 'd&b audiotechnik', specs: { weight: '32 kg', power: '1200 W', freq_range: '55 Hz - 18 kHz' }, image: '' },
+ { id: 3, code: 'LT-003', name: 'Shure SM58', category: 'Tontechnik', description: 'Dynamisches Gesangsmikrofon, Kardioid-Richtcharakteristik, Industrie-Standard seit Jahrzehnten', brand: 'Shure', specs: { weight: '330 g', type: 'dynamisch', polar: 'Kardioid' }, image: '' },
+ { id: 4, code: 'LT-004', name: 'Allen & Heath dLive S5000', category: 'Tontechnik', description: 'Digitales Mischpult mit 96 Input Fadern, 64 Mix Buses, Dante-kompatibel für große Live-Produktionen', brand: 'Allen & Heath', specs: { channels: '96', buses: '64', fx: '16 Effekt-Engines' }, image: '' },
+ { id: 5, code: 'LL-001', name: 'Robe Pointe', category: 'Lichttechnik', description: '230W Moving Beam mit 5°-20° Zoom, 16-bit Dimming, vielseitig für Beam- und Wash-Anwendungen', brand: 'Robe', specs: { power: '230 W', source: 'MSD 230', zoom: '5°-20°' }, image: '' },
+ { id: 6, code: 'LL-002', name: 'Mac Aura PXL', category: 'Lichttechnik', description: 'RGBW Wash-Light mit 18 pixel-mappable Zellen, Zoom 4°-60°, Art-Net steuerbar', brand: 'Martin', specs: { power: '700 W', cells: '18', zoom: '4°-60°' }, image: '' },
+ { id: 7, code: 'LL-003', name: 'Showtec Phantom 140', category: 'Lichttechnik', description: '140W LED Spot Moving Head, 9 rotierende + 9 feste Gobos, 8-fach Prisma', brand: 'Showtec', specs: { power: '140 W', gobos: '9 rotierend + 9 fest', zoom: '12°-18°' }, image: '' },
+ { id: 8, code: 'LL-004', name: 'Astera Titan Tube', category: 'Lichttechnik', description: 'Pixel Tube 1m, batteriebetrieben mit 20 Std. Laufzeit, DMX/CRMX-Steuerung, RGBW', brand: 'Astera', specs: { length: '1015 mm', battery: '20 Std', cells: '16 Pixel' }, image: '' },
+ { id: 9, code: 'RG-001', name: 'Tomcat Truss 290', category: 'Rigging', description: 'Aluminium Traverse 4-Punkt, 290mm Profil, 2m Länge, belastbar bis 450kg freitragend', brand: 'Tomcat', specs: { profile: '290x290 mm', length: '2 m', load: '450 kg' }, image: '' },
+ { id: 10, code: 'RG-002', name: 'Chain Motor 1T', category: 'Rigging', description: 'Bühnen-Motor 1000kg, D8+ Plus geprüft, inklusive Steuerkabel und Lasthaken', brand: 'Verlinde', specs: { capacity: '1000 kg', speed: '4-8 m/min', control: 'Direct Control' }, image: '' },
+ { id: 11, code: 'VD-001', name: 'LED Video Wall P3.9', category: 'Video', description: 'Indoor LED Panel P3.9, 500x500mm, 3840Hz Refresh Rate, 1500 nits für Bühnenhintergründe', brand: 'Absen', specs: { pixel_pitch: '3.9 mm', size: '500x500 mm', refresh: '3840 Hz' }, image: '' },
+ { id: 12, code: 'VD-002', name: 'Blackmagic ATEM Mini Extreme', category: 'Video', description: '8-Input HDMI Switcher mit ISO Recording, Multiview und Direct Streaming', brand: 'Blackmagic', specs: { inputs: '8x HDMI', outputs: '2x HDMI', recording: 'ISO Recording' }, image: '' },
+ { id: 13, code: 'ST-001', name: 'WAGO 24V 40A Netzteilkasten', category: 'Strom', description: '24V DC 40A Netzteil in Flightcase, 4x Schuko Ausgang, für LED- und Steuerungstechnik', brand: 'WAGO', specs: { output: '24V DC 40A', inputs: '4x Schuko', protection: 'IP54' }, image: '' },
+ { id: 14, code: 'ST-002', name: 'Duraplex 32A Stromverteiler', category: 'Strom', description: '32A 5-polig auf 3x 16A + 3x Schuko, mit FI-Schutzschalter, professionelle Bühnenstromversorgung', brand: 'Duraplex', specs: { input: '32A 5-pol CEE', outputs: '3x 16A + 3x Schuko', protection: 'FI/A' }, image: '' },
+ { id: 15, code: 'ZB-001', name: 'Flightcase 19" 12HE', category: 'Zubehör', description: '19" Rack Flightcase 12HE, Front- und Rücktür, 4x 100mm Rollen, Birkenholz mit Alu-Kanten', brand: 'Thon', specs: { rack_units: '12 HE', depth: '600 mm', wheels: '4x 100mm' }, image: '' },
+ { id: 16, code: 'ZB-002', name: 'XLR Kabel 20m', category: 'Zubehör', description: 'XLR3 male/female Mikrofonkabel, 20m, Neutrik Stecker, doppelt geschirmt', brand: 'Tasker', specs: { length: '20 m', connectors: 'XLR3 M/F', shielding: 'double' }, image: '' },
+ { id: 17, code: 'ZB-003', name: 'DJ-Table Pro', category: 'Zubehör', description: 'Mobile DJ-Tisch mit Laptop-Ständer und integrierter LED-Beleuchtung, transportiert im Flightcase', brand: 'Stage Traps', specs: { width: '120 cm', height: '95 cm', features: 'LED, Laptop-Ständer' }, image: '' },
+ { id: 18, code: 'LT-005', name: 'Sennheiser EW-DX 835', category: 'Tontechnik', description: 'Digitales Funkmikrofon-Set, Handheld + Empfänger, 100m Reichweite, 12 Std. Akkulaufzeit', brand: 'Sennheiser', specs: { type: 'Digital Wireless', range: '100 m', battery: '12 Std' }, image: '' }
+ ];
+ const filteredEquipment = computed(() => {
+ let items = allEquipment;
+ if (activeCategory.value !== 'alle') items = items.filter(i => i.category === activeCategory.value);
+ if (searchQuery.value.trim()) { const q = searchQuery.value.toLowerCase(); items = items.filter(i => i.name.toLowerCase().includes(q) || i.code.toLowerCase().includes(q) || i.brand.toLowerCase().includes(q)); }
+ if (sortBy.value === 'name') items = [...items].sort((a,b) => a.name.localeCompare(b.name));
+ else if (sortBy.value === 'brand') items = [...items].sort((a,b) => a.brand.localeCompare(b.brand));
+ else if (sortBy.value === 'code') items = [...items].sort((a,b) => a.code.localeCompare(b.code));
+ return items;
+ });
+ onMounted(() => { setTimeout(() => { loading.value = false; }, 1000); });
+ function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
+ function navigateToDetail(item) { window.location.hash = '#/mietkatalog/' + item.id; window.scrollTo(0, 0); }
+ function retry() { loading.value = true; error.value = false; setTimeout(() => { loading.value = false; }, 800); }
+ return { loading, error, searchQuery, activeCategory, sortBy, categories, filteredEquipment, navigate, navigateToDetail, retry };
+ },
+ template: `
+
+
+
Mietkatalog Durchsuchen Sie unser Equipment-Sortiment und stellen Sie Ihre Mietanfrage zusammen.
+
🛒 Warenkorb ansehen
+
+
+
+
+
Sortieren: Name (A-Z) Sortieren: Marke (A-Z) Sortieren: Artikelnummer
+
+
{{ cat === 'alle' ? 'Alle Kategorien' : cat }}
+
+
{{ filteredEquipment.length }} {{ filteredEquipment.length === 1 ? 'Gerät' : 'Geräte' }} gefunden
+
+
+
+
+
`
+});
+
+// ===== EQUIPMENT DETAIL PAGE =====
+const EquipmentDetailPage = defineComponent({
+ name: 'EquipmentDetailPage',
+ props: { id: [String, Number], cart: Array },
+ emits: ['add-to-cart', 'navigate'],
+ setup(props, { emit }) {
+ const loading = ref(true); const error = ref(false); const quantity = ref(1);
+ const rentalStart = ref(''); const rentalEnd = ref(''); const item = ref(null);
+ const equipmentData = [
+ { id: 1, code: 'LT-001', name: 'L-Acoustics K2', category: 'Tontechnik', description: 'Das L-Acoustics K2 ist das Flaggschiff der Line-Array-Serie und eignet sich für große Open-Air-Veranstaltungen und Hallenbeschallung.', brand: 'L-Acoustics', specs: { weight: '56 kg', power: '1440 W', freq_range: '35 Hz - 20 kHz', max_spl: '142 dB', coverage: '10°-110° variable' }, image: '', related: [2, 4, 3] },
+ { id: 2, code: 'LT-002', name: 'd&b audiotechnik KSL8', category: 'Tontechnik', description: 'Bi-amplified Column Array Loudspeaker mit 80° horizontaler Abstrahlung.', brand: 'd&b audiotechnik', specs: { weight: '32 kg', power: '1200 W', freq_range: '55 Hz - 18 kHz', max_spl: '138 dB', coverage: '80° horizontal' }, image: '', related: [1, 4, 18] },
+ { id: 3, code: 'LT-003', name: 'Shure SM58', category: 'Tontechnik', description: 'Das weltweit meistverkaufte dynamische Gesangsmikrofon.', brand: 'Shure', specs: { weight: '330 g', type: 'dynamisch', polar: 'Kardioid', freq_range: '50 Hz - 15 kHz', connector: 'XLR3' }, image: '', related: [4, 18, 1] },
+ { id: 4, code: 'LT-004', name: 'Allen & Heath dLive S5000', category: 'Tontechnik', description: 'Professionelles digitales Mischpult mit 96 Input Fadern und 64 Mix Buses.', brand: 'Allen & Heath', specs: { channels: '96', buses: '64', fx: '16 Effekt-Engines', screens: '2x 15" Touch', i_o: '128x128 Dante' }, image: '', related: [1, 2, 3] },
+ { id: 5, code: 'LL-001', name: 'Robe Pointe', category: 'Lichttechnik', description: '230W Moving Beam mit 5°-20° Zoom und 16-bit Dimming.', brand: 'Robe', specs: { power: '230 W', source: 'MSD 230', zoom: '5°-20°', dimming: '16-bit', pan: '540°', tilt: '270°' }, image: '', related: [6, 7, 8] },
+ { id: 6, code: 'LL-002', name: 'Mac Aura PXL', category: 'Lichttechnik', description: 'RGBW Wash-Light mit 18 einzelnen pixel-mappable Zellen.', brand: 'Martin', specs: { power: '700 W', cells: '18', zoom: '4°-60°', control: 'DMX 512, Art-Net', color: 'RGBW' }, image: '', related: [5, 7, 8] },
+ { id: 7, code: 'LL-003', name: 'Showtec Phantom 140', category: 'Lichttechnik', description: '140W LED Spot Moving Head mit 9 rotierenden und 9 festen Gobos.', brand: 'Showtec', specs: { power: '140 W', gobos: '9 rotierend + 9 fest', zoom: '12°-18°', color: '7+1 Farbrad', prism: '8-fach rotierend' }, image: '', related: [5, 6, 8] },
+ { id: 8, code: 'LL-004', name: 'Astera Titan Tube', category: 'Lichttechnik', description: 'Pixel Tube 1m Länge, batteriebetrieben mit 20 Std. Laufzeit.', brand: 'Astera', specs: { length: '1015 mm', battery: '20 Std', cells: '16 Pixel', control: 'DMX, CRMX, App', color: 'RGBW' }, image: '', related: [5, 6, 7] },
+ { id: 9, code: 'RG-001', name: 'Tomcat Truss 290', category: 'Rigging', description: 'Aluminium Traverse 4-Punkt, 290mm Profil, 2m Länge.', brand: 'Tomcat', specs: { profile: '290x290 mm', length: '2 m', load: '450 kg', material: 'Aluminium EN AW-6082' }, image: '', related: [10, 14, 15] },
+ { id: 10, code: 'RG-002', name: 'Chain Motor 1T', category: 'Rigging', description: 'Bühnen-Motor 1000kg Tragkraft, D8+ Plus geprüft.', brand: 'Verlinde', specs: { capacity: '1000 kg', speed: '4-8 m/min', control: 'Direct Control', certification: 'D8+ Plus' }, image: '', related: [9, 14, 13] },
+ { id: 11, code: 'VD-001', name: 'LED Video Wall P3.9', category: 'Video', description: 'Indoor LED Panel P3.9, 500x500mm, 3840Hz Refresh Rate.', brand: 'Absen', specs: { pixel_pitch: '3.9 mm', size: '500x500 mm', refresh: '3840 Hz', brightness: '1500 nits', ip: 'IP30 Indoor' }, image: '', related: [12, 5, 6] },
+ { id: 12, code: 'VD-002', name: 'Blackmagic ATEM Mini Extreme', category: 'Video', description: '8-Input HDMI Switcher mit ISO Recording und Multiview.', brand: 'Blackmagic', specs: { inputs: '8x HDMI', outputs: '2x HDMI', recording: 'ISO Recording', streaming: 'Direct Streaming', audio: '2x 3.5mm' }, image: '', related: [11, 5, 7] },
+ { id: 13, code: 'ST-001', name: 'WAGO 24V 40A Netzteilkasten', category: 'Strom', description: '24V DC 40A Netzteil in Flightcase mit 4x Schuko Ausgang.', brand: 'WAGO', specs: { output: '24V DC 40A', inputs: '4x Schuko', protection: 'IP54', cooling: 'Forced Air' }, image: '', related: [14, 10, 9] },
+ { id: 14, code: 'ST-002', name: 'Duraplex 32A Stromverteiler', category: 'Strom', description: '32A 5-polig auf 3x 16A + 3x Schuko mit FI-Schutzschalter.', brand: 'Duraplex', specs: { input: '32A 5-pol CEE', outputs: '3x 16A + 3x Schuko', protection: 'FI/A', cable: 'H07BQ-F 5G6' }, image: '', related: [13, 10, 9] },
+ { id: 15, code: 'ZB-001', name: 'Flightcase 19" 12HE', category: 'Zubehör', description: '19" Rack Flightcase 12HE mit Front- und Rücktür.', brand: 'Thon', specs: { rack_units: '12 HE', depth: '600 mm', wheels: '4x 100mm', material: 'Birkenholz + Alu-Kanten' }, image: '', related: [16, 4, 15] },
+ { id: 16, code: 'ZB-002', name: 'XLR Kabel 20m', category: 'Zubehör', description: 'XLR3 male/female Mikrofonkabel, 20m, Neutrik Stecker.', brand: 'Tasker', specs: { length: '20 m', connectors: 'XLR3 M/F', shielding: 'double', cable: 'Tasker C118' }, image: '', related: [3, 18, 4] },
+ { id: 17, code: 'ZB-003', name: 'DJ-Table Pro', category: 'Zubehör', description: 'Mobile DJ-Tisch mit Laptop-Ständer und LED-Beleuchtung.', brand: 'Stage Traps', specs: { width: '120 cm', height: '95 cm', features: 'LED, Laptop-Ständer', weight: '22 kg' }, image: '', related: [12, 5, 15] },
+ { id: 18, code: 'LT-005', name: 'Sennheiser EW-DX 835', category: 'Tontechnik', description: 'Digitales Funkmikrofon-Set mit Handheld und Empfänger.', brand: 'Sennheiser', specs: { type: 'Digital Wireless', range: '100 m', battery: '12 Std', freq_range: '563-608 MHz', channels: 'gleichzeitig 95' }, image: '', related: [3, 4, 16] }
+ ];
+ const relatedItems = computed(() => { if (!item.value || !item.value.related) return []; return item.value.related.map(id => equipmentData.find(e => e.id === id)).filter(Boolean); });
+ onMounted(() => { setTimeout(() => { const found = equipmentData.find(e => e.id === Number(props.id)); if (found) { item.value = found; } else { error.value = true; } loading.value = false; }, 800); });
+ function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
+ function addToCart() { emit('add-to-cart', { ...item.value, quantity: quantity.value, rentalStart: rentalStart.value, rentalEnd: rentalEnd.value }); navigate('#/warenkorb'); }
+ function addToCartSimple(e) { emit('add-to-cart', e); }
+ return { loading, error, item, quantity, rentalStart, rentalEnd, relatedItems, navigate, addToCart, addToCartSimple };
+ },
+ template: `
+
+
← Zurück zum Katalog
+
+
+
+
+
+
+
+
{{ item.category }}
+
+
+
Artikelnummer: {{ item.code }}
+
{{ item.name }}
+
Marke: {{ item.brand }}
+
{{ item.description }}
+
Technische Daten
{{ key.replace(/_/g, ' ') }} {{ value }}
+
+
Mietanfrage
+
+
+
Zur Mietanfrage hinzufügen
+
Preise auf Anfrage – unverbindliche Mietanfrage
+
+
+
+
+
+
`
+});
+
+// ===== WARENKORB PAGE =====
+const WarenkorbPage = defineComponent({
+ name: 'WarenkorbPage',
+ props: { cart: Array },
+ emits: ['remove-item', 'update-quantity', 'navigate', 'clear-cart'],
+ setup(props, { emit }) {
+ const submitting = ref(false); const submitted = ref(false);
+ const form = reactive({ name: '', email: '', phone: '', event_date: '', event_location: '', message: '' });
+ const errors = reactive({});
+ function removeItem(index) { emit('remove-item', index); }
+ function updateQty(index, delta) { emit('update-quantity', { index, delta }); }
+ function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
+ function validate() {
+ const e = {};
+ if (!form.name.trim()) e.name = 'Name ist erforderlich';
+ if (!form.email.trim()) e.email = 'E-Mail ist erforderlich';
+ else if (!/^\S+@\S+\.\S+$/.test(form.email)) e.email = 'Ungültige E-Mail';
+ if (!form.event_date) e.event_date = 'Veranstaltungsdatum erforderlich';
+ if (!form.event_location.trim()) e.event_location = 'Veranstaltungsort erforderlich';
+ Object.keys(errors).forEach(k => delete errors[k]); Object.assign(errors, e);
+ return Object.keys(e).length === 0;
+ }
+ function submitRequest() { if (!validate()) return; submitting.value = true; setTimeout(() => { submitting.value = false; submitted.value = true; emit('clear-cart'); }, 1800); }
+ function resetForm() { Object.assign(form, { name: '', email: '', phone: '', event_date: '', event_location: '', message: '' }); submitted.value = false; }
+ return { submitting, submitted, form, errors, removeItem, updateQty, navigate, validate, submitRequest, resetForm };
+ },
+ template: `
+
+
Mietanfrage
+
Überprüfen Sie Ihre Geräteauswahl und senden Sie die Anfrage ab.
+
+
+
Mietanfrage übermittelt
+
Vielen Dank! Wir melden uns innerhalb von 24 Stunden mit einem unverbindlichen Angebot.
+
Weiter stöbern Zur Startseite
+
+
+
+
+
+
+
📦
+
+
{{ item.name }} {{ item.code }} · {{ item.brand }}
+
+
+
− {{ item.quantity }} +
📅 {{ item.rentalStart }} bis {{ item.rentalEnd }}
+
+
+
+
Warenkorb leeren
+
+
+
+
Anfrage senden
+
Geräte gesamt: {{ cart.reduce((s,i)=>s+i.quantity,0) }}
Positionen: {{ cart.length }}
+
+
+
+
+
Telefon
+
+
+
Anmerkung
+
Anfrage absenden
+
Unverbindlich – Angebot innerhalb 24 Std.
+
+
+
+
+
+
`
+});
+
+// ===== ADMIN PAGE =====
+const AdminPage = defineComponent({
+ name: 'AdminPage',
+ setup() {
+ const username = ref(''); const password = ref(''); const error = ref(''); const loading = ref(false); const loggedIn = ref(false);
+ function login() { error.value = ''; if (!username.value || !password.value) { error.value = 'Bitte Benutzername und Passwort eingeben'; return; } loading.value = true; setTimeout(() => { loading.value = false; loggedIn.value = true; }, 1200); }
+ return { username, password, error, loading, loggedIn, login };
+ },
+ template: `
+ Admin-Login Equipment-Sync Verwaltungsbereich
+
Eingeloggt
(Prototyp – keine echte Session)
+
`
+});
+
+// ===== 404 =====
+const NotFoundPage = defineComponent({
+ name: 'NotFoundPage',
+ template: `404
Seite nicht gefunden Die angeforderte Seite existiert nicht.
Zur Startseite `
+});
+
+// ===== MAIN APP =====
+const App = defineComponent({
+ name: 'App',
+ setup() {
+ const currentRoute = ref(window.location.hash || '#/');
+ const cart = reactive([]);
+ function addToCart(item) { const existing = cart.find(i => i.id === item.id); if (existing) { existing.quantity += item.quantity || 1; } else { cart.push({ ...item, quantity: item.quantity || 1 }); } }
+ function removeItem(index) { cart.splice(index, 1); }
+ function updateQuantity({ index, delta }) { const item = cart[index]; if (item) { item.quantity = Math.max(1, item.quantity + delta); } }
+ function clearCart() { cart.splice(0, cart.length); }
+ const routeParts = computed(() => { const hash = currentRoute.value.replace(/^#/, '') || '/'; return hash.split('/').filter(Boolean); });
+ const currentView = computed(() => { const parts = routeParts.value; if (parts.length === 0) return 'home'; if (parts[0] === 'referenzen') return 'referenzen'; if (parts[0] === 'kontakt') return 'kontakt'; if (parts[0] === 'mietkatalog') return parts.length > 1 ? 'equipment-detail' : 'mietkatalog'; if (parts[0] === 'warenkorb') return 'warenkorb'; if (parts[0] === 'admin') return 'admin'; return '404'; });
+ const detailId = computed(() => { const parts = routeParts.value; if (parts[0] === 'mietkatalog' && parts.length > 1) return parts[1]; return null; });
+ window.addEventListener('hashchange', () => { currentRoute.value = window.location.hash || '#/'; window.scrollTo(0, 0); });
+ return { currentRoute, currentView, detailId, cart, addToCart, removeItem, updateQuantity, clearCart };
+ },
+ template: `
+ `
+});
+
+const app = createApp(App);
+app.component('hms-logo', HmsLogo);
+app.component('speaker-icon', SpeakerIcon);
+app.component('app-header', AppHeader);
+app.component('app-footer', AppFooter);
+app.component('service-card', ServiceCard);
+app.component('equipment-card', EquipmentCard);
+app.component('loading-skeleton', LoadingSkeleton);
+app.component('empty-state', EmptyState);
+app.component('error-state', ErrorState);
+app.component('home-page', HomePage);
+app.component('referenzen-page', ReferenzenPage);
+app.component('kontakt-page', KontaktPage);
+app.component('mietkatalog-page', MietkatalogPage);
+app.component('equipment-detail-page', EquipmentDetailPage);
+app.component('warenkorb-page', WarenkorbPage);
+app.component('admin-page', AdminPage);
+app.component('not-found-page', NotFoundPage);
+app.mount('#app');
diff --git a/docs/prototype-index.html b/docs/prototype-index.html
new file mode 100644
index 0000000..f488e49
--- /dev/null
+++ b/docs/prototype-index.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HMS Licht & Ton – Veranstaltungstechnik Leipheim/Ellzee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css
index b44a1c2..dc0a1aa 100644
--- a/frontend/assets/css/main.css
+++ b/frontend/assets/css/main.css
@@ -1,31 +1,71 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* ============================================
+ HMS Licht & Ton – Design Tokens & Custom CSS
+ Dark Theme + Subtle Orange Accent
+ ============================================ */
+
:root {
+ /* Brand Accent - used sparingly */
+ --color-accent: #EC6925;
+ --color-accent-hover: #d4581a;
+ --color-accent-light: rgba(236, 105, 37, 0.08);
+ --color-accent-border: rgba(236, 105, 37, 0.25);
+ --color-accent-dark: #b8461a;
+
+ /* Agent Zero Dark Theme Grays */
--bg: #131313;
--panel: #1a1a1a;
--surface: #212121;
--row: #272727;
--card: #2d2d2d;
--border: #444444a8;
- --border-strong: #555555;
--secondary: #656565;
--primary: #737a81;
--text: #ffffff;
--text-muted: #d4d4d4;
- --color-accent: #EC6925;
- --color-accent-hover: #d4581a;
- --color-accent-light: rgba(236, 105, 37, 0.08);
- --color-accent-border: rgba(236, 105, 37, 0.25);
- --color-accent-dark: #b8461a;
+
+ /* Aliases */
+ --color-bg: var(--bg);
+ --color-surface: var(--panel);
+ --color-surface-alt: var(--surface);
+ --color-card: var(--card);
+ --color-text: var(--text);
+ --color-text-muted: var(--text-muted);
+ --color-text-light: var(--secondary);
+ --color-border: var(--border);
+ --color-border-strong: #555555;
+ --border-strong: #555555;
+
+ /* Status Colors */
--color-success: #4ade80;
--color-success-bg: rgba(74, 222, 128, 0.08);
--color-error: #f87171;
--color-error-bg: rgba(248, 113, 113, 0.08);
--color-warning: #fbbf24;
+ --color-warning-bg: rgba(251, 191, 36, 0.08);
--color-info: #60a5fa;
- --radius-sm: 2px;
- --radius-md: 3px;
- --radius-lg: 4px;
- --radius-xl: 4px;
- --radius-full: 9999px;
+ --color-info-bg: rgba(96, 165, 250, 0.08);
+
+ /* Typography */
+ --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
+ --font-heading: 'Inter', system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
+
+ --text-xs: 0.75rem;
+ --text-sm: 0.875rem;
+ --text-base: 1rem;
+ --text-lg: 1.125rem;
+ --text-xl: 1.25rem;
+ --text-2xl: 1.5rem;
+ --text-3xl: 1.875rem;
+ --text-4xl: 2.25rem;
+ --text-5xl: 3rem;
+ --text-6xl: 3.75rem;
+
+ /* Spacing */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
@@ -34,149 +74,359 @@
--space-2xl: 3rem;
--space-3xl: 4rem;
--space-4xl: 6rem;
+
+ /* Radius */
+ --radius-sm: 2px;
+ --radius-md: 3px;
+ --radius-lg: 4px;
+ --radius-xl: 4px;
+ --radius-full: 9999px;
+
+ /* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.3);
+
+ /* Transitions */
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 400ms ease;
+
+ /* Layout */
+ --header-height: 4rem;
+ --max-width: 1280px;
}
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- html {
- scroll-behavior: smooth;
- }
-
- body {
- background-color: var(--bg);
- color: var(--text);
- font-family: "Inter", system-ui, sans-serif;
- line-height: 1.6;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
-
- h1, h2, h3, h4, h5, h6 {
- line-height: 1.2;
- font-weight: 700;
- }
-
- *:focus-visible {
- outline: 2px solid var(--color-accent);
- outline-offset: 2px;
- }
-
- a {
- color: inherit;
- text-decoration: none;
- transition: color var(--transition-fast);
- }
+/* Base Reset */
+* { box-sizing: border-box; margin: 0; padding: 0; }
+html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
+body {
+ font-family: var(--font-sans);
+ color: var(--text);
+ background-color: var(--bg);
+ line-height: 1.6;
+ font-size: var(--text-base);
+ -webkit-font-smoothing: antialiased;
}
-@layer components {
- .skip-link {
- position: absolute;
- left: -9999px;
- top: 0;
- z-index: 100;
- padding: 0.5rem 1rem;
- background: var(--color-accent);
- color: #fff;
- border-radius: var(--radius-md);
- }
+/* Tailwind dark overrides */
+.text-gray-900 { color: var(--text) !important; }
+.text-gray-800 { color: var(--text-muted) !important; }
+.text-gray-700 { color: var(--text-muted) !important; }
+.text-gray-600 { color: var(--text-muted) !important; }
+.text-gray-500 { color: var(--secondary) !important; }
+.text-gray-400 { color: var(--secondary) !important; }
+.text-white { color: var(--text) !important; }
+.bg-white { background-color: var(--panel) !important; }
+.bg-gray-100 { background-color: var(--surface) !important; }
+.bg-gray-200 { background-color: var(--row) !important; }
+.bg-gray-800 { background-color: var(--bg) !important; }
+.bg-gray-900 { background-color: var(--bg) !important; }
+.border-gray-200 { border-color: var(--border) !important; }
+.border-gray-700 { border-color: var(--border) !important; }
+.border-gray-100 { border-color: var(--border) !important; }
+.divide-gray-100 > * { border-color: var(--border) !important; }
- .skip-link:focus {
- left: 0.5rem;
- top: 0.5rem;
- }
+/* Skip Link */
+.skip-link {
+ position: absolute; top: -100px; left: 0;
+ background: var(--color-accent); color: white;
+ padding: var(--space-md) var(--space-lg);
+ z-index: 9999; border-radius: 0 0 var(--radius-md) 0;
+ font-weight: 600;
+}
+.skip-link:focus { top: 0; }
- .nav-link {
- position: relative;
- color: var(--text-muted);
- font-weight: 500;
- font-size: 0.875rem;
- transition: color var(--transition-fast);
- padding: 0.5rem 0;
- min-height: 44px;
- display: flex;
- align-items: center;
- }
-
- .nav-link:hover {
- color: var(--color-accent);
- }
-
- .nav-link::after {
- content: "";
- position: absolute;
- bottom: 0;
- left: 0;
- width: 0;
- height: 2px;
- background-color: var(--color-accent);
- transition: width var(--transition-fast);
- }
-
- .nav-link:hover::after,
- .nav-link.router-link-active::after {
- width: 100%;
- }
-
- .nav-link.router-link-active {
- color: var(--color-accent);
- }
-
- .btn-primary {
- background-color: var(--color-accent);
- color: #ffffff;
- border-radius: var(--radius-md);
- padding: 0.625rem 1.5rem;
- font-weight: 600;
- font-size: 0.875rem;
- transition: background-color var(--transition-fast);
- min-height: 44px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
-
- .btn-primary:hover {
- background-color: var(--color-accent-hover);
- }
-
- .btn-secondary {
- background-color: var(--surface);
- color: var(--text-muted);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 0.625rem 1.5rem;
- font-weight: 500;
- font-size: 0.875rem;
- transition: all var(--transition-fast);
- min-height: 44px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
-
- .btn-secondary:hover {
- border-color: var(--color-accent-border);
- color: var(--text);
- }
+/* Focus visible */
+*:focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm);
}
-@keyframes shimmer {
- 0% { background-position: -200% 0; }
- 100% { background-position: 200% 0; }
+/* Scrollbar */
+::-webkit-scrollbar { width: 8px; height: 8px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 4px; }
+::-webkit-scrollbar-thumb:hover { background: var(--primary); }
+
+/* Component: Header */
+.hms-header {
+ background: var(--panel);
+ border-bottom: 1px solid var(--border);
+ position: sticky; top: 0; z-index: 100;
+}
+.hms-logo-svg { height: 40px; width: auto; }
+.hms-logo-svg rect { stroke: var(--color-accent) !important; }
+
+/* Component: Hero – with image background, subtle orange */
+.hms-hero {
+ position: relative;
+ background: var(--bg);
+ color: var(--text);
+ overflow: hidden;
+}
+.hms-hero-bg {
+ position: absolute;
+ inset: 0;
+ background-size: cover;
+ background-position: center;
+ opacity: 0.35;
+}
+.hms-hero-overlay {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, rgba(19,19,19,0.6) 0%, rgba(19,19,19,0.85) 50%, var(--bg) 100%);
}
-.skeleton-shimmer {
- background: linear-gradient(90deg, var(--panel) 25%, var(--surface) 50%, var(--panel) 75%);
+/* Component: Card */
+.hms-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ transition: box-shadow var(--transition-base), border-color var(--transition-base);
+}
+.hms-card:hover {
+ box-shadow: var(--shadow-lg);
+ border-color: var(--secondary);
+}
+
+/* Component: Button – orange only on primary, subtle */
+.hms-btn {
+ display: inline-flex; align-items: center; justify-content: center;
+ gap: var(--space-sm);
+ font-family: var(--font-sans); font-weight: 600;
+ border-radius: var(--radius-md);
+ transition: all var(--transition-fast);
+ cursor: pointer; border: none; text-decoration: none;
+ white-space: nowrap;
+}
+.hms-btn-primary {
+ background: var(--color-accent); color: white;
+ padding: var(--space-md) var(--space-xl);
+ box-shadow: 0 0 0 0 rgba(236, 105, 37, 0);
+}
+.hms-btn-primary:hover {
+ background: var(--color-accent-hover);
+ box-shadow: 0 0 12px 2px rgba(236, 105, 37, 0.3);
+ transform: translateY(-1px);
+}
+.hms-btn-primary:active { transform: translateY(0); box-shadow: 0 0 6px 0px rgba(236, 105, 37, 0.2); }
+.hms-btn-primary:disabled { background: var(--secondary); cursor: not-allowed; box-shadow: none; transform: none; }
+
+.hms-btn-secondary {
+ background: var(--surface); color: var(--text-muted);
+ border: 1px solid var(--border-strong);
+ padding: var(--space-md) var(--space-xl);
+}
+.hms-btn-secondary:hover {
+ background: var(--row);
+ border-color: var(--color-accent);
+ color: var(--text);
+ box-shadow: 0 0 8px 0 rgba(236, 105, 37, 0.1);
+}
+.hms-btn-secondary:active { background: var(--surface); box-shadow: none; }
+
+.hms-btn-ghost {
+ background: transparent; color: var(--text-muted);
+ padding: var(--space-sm) var(--space-md);
+}
+.hms-btn-ghost:hover { background: var(--surface); color: var(--color-accent); border-color: var(--color-accent-border); }
+
+/* Component: Nav Button (header menu items) */
+.hms-nav-btn {
+ position: relative;
+ transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast) !important;
+}
+.hms-nav-btn:hover {
+ color: var(--color-accent) !important;
+ background: var(--color-accent-light) !important;
+}
+.hms-nav-btn:hover::after {
+ content: '';
+ position: absolute;
+ bottom: 2px; left: 50%;
+ transform: translateX(-50%);
+ width: 60%; height: 2px;
+ background: var(--color-accent);
+ border-radius: var(--radius-full);
+}
+.hms-nav-btn[aria-current="page"]:hover {
+ color: var(--color-accent) !important;
+ background: var(--color-accent-light) !important;
+}
+
+/* Component: Badge – subtle orange */
+.hms-badge {
+ display: inline-flex; align-items: center;
+ padding: 2px var(--space-sm);
+ border-radius: var(--radius-full);
+ font-size: var(--text-xs); font-weight: 600;
+}
+.hms-badge-primary {
+ background: rgba(236, 105, 37, 0.12);
+ color: var(--color-accent);
+ border: 1px solid rgba(236, 105, 37, 0.2);
+}
+.hms-badge-gray { background: var(--surface); color: var(--text-muted); }
+.hms-badge-success { background: var(--color-success-bg); color: var(--color-success); }
+.hms-badge-error { background: var(--color-error-bg); color: var(--color-error); }
+
+/* Component: Input */
+.hms-input {
+ width: 100%;
+ padding: var(--space-md) var(--space-lg);
+ border: 1px solid var(--border-strong);
+ border-radius: var(--radius-md);
+ font-size: var(--text-base);
+ background: var(--surface); color: var(--text);
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+}
+.hms-input::placeholder { color: var(--secondary); }
+.hms-input:focus {
+ outline: none;
+ border-color: var(--color-accent);
+ box-shadow: 0 0 0 2px rgba(236, 105, 37, 0.1);
+}
+.hms-input:disabled { background: var(--row); color: var(--secondary); cursor: not-allowed; }
+.hms-input-error { border-color: var(--color-error); }
+.hms-input-error:focus { box-shadow: 0 0 0 2px rgba(248, 113, 113, 0.1); }
+select.hms-input { background: var(--surface); color: var(--text); }
+select.hms-input option { background: var(--panel); color: var(--text); }
+
+/* Component: Loading Skeleton */
+.hms-skeleton {
+ background: linear-gradient(90deg, var(--surface) 25%, var(--row) 50%, var(--surface) 75%);
background-size: 200% 100%;
- animation: shimmer 1.5s infinite;
+ animation: skeleton-shimmer 1.5s infinite;
+ border-radius: var(--radius-md);
+}
+@keyframes skeleton-shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
+
+/* Component: Spinner */
+.hms-spinner {
+ width: 24px; height: 24px;
+ border: 3px solid var(--surface);
+ border-top-color: var(--color-accent);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+
+/* Component: Service Icon Circle – subtle, no large orange area */
+.hms-icon-circle {
+ width: 48px; height: 48px;
+ border-radius: var(--radius-md);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.25rem; flex-shrink: 0;
+}
+
+/* Component: Speaker Grid */
+.hms-speaker-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: var(--space-md);
+}
+.hms-speaker-item {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ text-align: center;
+ transition: all var(--transition-base);
+}
+.hms-speaker-item:hover {
+ border-color: var(--color-accent-border);
+ background: var(--surface);
+}
+.hms-speaker-icon-wrap { display: inline-flex; align-items: center; justify-content: center; margin: 0 auto var(--space-sm); }
+.hms-speaker-icon-wrap svg { width: 48px; height: 48px; color: var(--text-muted); transition: color var(--transition-base); }
+.hms-speaker-item:hover .hms-speaker-icon-wrap svg { color: var(--color-accent); }
+.hms-speaker-name {
+ font-size: var(--text-sm);
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 2px;
+}
+.hms-speaker-type {
+ font-size: var(--text-xs);
+ color: var(--secondary);
+}
+
+/* Component: Footer */
+.hms-footer {
+ background: var(--bg);
+ color: var(--text-muted);
+ border-top: 1px solid var(--border);
+}
+
+/* Gallery Grid */
+.hms-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: var(--space-lg);
+}
+
+/* Equipment Card */
+.hms-eq-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ transition: all var(--transition-base);
+ cursor: pointer;
+}
+.hms-eq-card:hover {
+ border-color: var(--color-accent-border);
+ box-shadow: var(--shadow-lg);
+ transform: translateY(-2px);
+}
+
+/* Filter Chip */
+.hms-chip {
+ display: inline-flex; align-items: center; gap: var(--space-xs);
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-full);
+ font-size: var(--text-sm); font-weight: 500;
+ cursor: pointer;
+ border: 1px solid var(--border-strong);
+ background: var(--surface); color: var(--text-muted);
+ transition: all var(--transition-fast);
+}
+.hms-chip:hover { border-color: var(--secondary); }
+.hms-chip.active {
+ background: var(--color-accent); color: white;
+ border-color: var(--color-accent);
+}
+
+/* Section divider – subtle */
+.hms-section-divider {
+ height: 1px;
+ background: linear-gradient(90deg, transparent 0%, var(--border) 50%, transparent 100%);
+}
+
+/* Mobile Menu Animation */
+.mobile-menu-enter-active, .mobile-menu-leave-active { transition: all var(--transition-base); }
+.mobile-menu-enter-from, .mobile-menu-leave-to { opacity: 0; transform: translateY(-10px); }
+
+/* Fade Transition */
+.fade-enter-active, .fade-leave-active { transition: opacity var(--transition-base); }
+.fade-enter-from, .fade-leave-to { opacity: 0; }
+
+/* Link colors */
+a { color: var(--color-accent); }
+a:hover { color: var(--color-accent-hover); }
+
+/* Responsive */
+@media (max-width: 768px) {
+ :root { --header-height: 3.5rem; }
+ .hms-speaker-grid { grid-template-columns: repeat(2, 1fr); }
}
diff --git a/frontend/assets/css/prototype-style.css b/frontend/assets/css/prototype-style.css
new file mode 100644
index 0000000..7400b2c
--- /dev/null
+++ b/frontend/assets/css/prototype-style.css
@@ -0,0 +1,427 @@
+/* ============================================
+ HMS Licht & Ton – Design Tokens & Custom CSS
+ Dark Theme + Subtle Orange Accent
+ ============================================ */
+
+:root {
+ /* Brand Accent - used sparingly */
+ --color-accent: #EC6925;
+ --color-accent-hover: #d4581a;
+ --color-accent-light: rgba(236, 105, 37, 0.08);
+ --color-accent-border: rgba(236, 105, 37, 0.25);
+ --color-accent-dark: #b8461a;
+
+ /* Agent Zero Dark Theme Grays */
+ --bg: #131313;
+ --panel: #1a1a1a;
+ --surface: #212121;
+ --row: #272727;
+ --card: #2d2d2d;
+ --border: #444444a8;
+ --secondary: #656565;
+ --primary: #737a81;
+ --text: #ffffff;
+ --text-muted: #d4d4d4;
+
+ /* Aliases */
+ --color-bg: var(--bg);
+ --color-surface: var(--panel);
+ --color-surface-alt: var(--surface);
+ --color-card: var(--card);
+ --color-text: var(--text);
+ --color-text-muted: var(--text-muted);
+ --color-text-light: var(--secondary);
+ --color-border: var(--border);
+ --color-border-strong: #555555;
+
+ /* Status Colors */
+ --color-success: #4ade80;
+ --color-success-bg: rgba(74, 222, 128, 0.08);
+ --color-error: #f87171;
+ --color-error-bg: rgba(248, 113, 113, 0.08);
+ --color-warning: #fbbf24;
+ --color-warning-bg: rgba(251, 191, 36, 0.08);
+ --color-info: #60a5fa;
+ --color-info-bg: rgba(96, 165, 250, 0.08);
+
+ /* Typography */
+ --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
+ --font-heading: 'Inter', system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
+
+ --text-xs: 0.75rem;
+ --text-sm: 0.875rem;
+ --text-base: 1rem;
+ --text-lg: 1.125rem;
+ --text-xl: 1.25rem;
+ --text-2xl: 1.5rem;
+ --text-3xl: 1.875rem;
+ --text-4xl: 2.25rem;
+ --text-5xl: 3rem;
+ --text-6xl: 3.75rem;
+
+ /* Spacing */
+ --space-xs: 0.25rem;
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 1.5rem;
+ --space-xl: 2rem;
+ --space-2xl: 3rem;
+ --space-3xl: 4rem;
+ --space-4xl: 6rem;
+
+ /* Radius */
+ --radius-sm: 2px;
+ --radius-md: 3px;
+ --radius-lg: 4px;
+ --radius-xl: 4px;
+ --radius-full: 9999px;
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.3);
+
+ /* Transitions */
+ --transition-fast: 150ms ease;
+ --transition-base: 250ms ease;
+ --transition-slow: 400ms ease;
+
+ /* Layout */
+ --header-height: 4rem;
+ --max-width: 1280px;
+}
+
+/* Base Reset */
+* { box-sizing: border-box; margin: 0; padding: 0; }
+html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
+body {
+ font-family: var(--font-sans);
+ color: var(--text);
+ background-color: var(--bg);
+ line-height: 1.6;
+ font-size: var(--text-base);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Tailwind dark overrides */
+.text-gray-900 { color: var(--text) !important; }
+.text-gray-800 { color: var(--text-muted) !important; }
+.text-gray-700 { color: var(--text-muted) !important; }
+.text-gray-600 { color: var(--text-muted) !important; }
+.text-gray-500 { color: var(--secondary) !important; }
+.text-gray-400 { color: var(--secondary) !important; }
+.text-white { color: var(--text) !important; }
+.bg-white { background-color: var(--panel) !important; }
+.bg-gray-100 { background-color: var(--surface) !important; }
+.bg-gray-200 { background-color: var(--row) !important; }
+.bg-gray-800 { background-color: var(--bg) !important; }
+.bg-gray-900 { background-color: var(--bg) !important; }
+.border-gray-200 { border-color: var(--border) !important; }
+.border-gray-700 { border-color: var(--border) !important; }
+.border-gray-100 { border-color: var(--border) !important; }
+.divide-gray-100 > * { border-color: var(--border) !important; }
+
+/* Skip Link */
+.skip-link {
+ position: absolute; top: -100px; left: 0;
+ background: var(--color-accent); color: white;
+ padding: var(--space-md) var(--space-lg);
+ z-index: 9999; border-radius: 0 0 var(--radius-md) 0;
+ font-weight: 600;
+}
+.skip-link:focus { top: 0; }
+
+/* Focus visible */
+*:focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm);
+}
+
+/* Scrollbar */
+::-webkit-scrollbar { width: 8px; height: 8px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 4px; }
+::-webkit-scrollbar-thumb:hover { background: var(--primary); }
+
+/* Component: Header */
+.hms-header {
+ background: var(--panel);
+ border-bottom: 1px solid var(--border);
+ position: sticky; top: 0; z-index: 100;
+}
+.hms-logo-svg { height: 40px; width: auto; }
+.hms-logo-svg rect { stroke: var(--color-accent) !important; }
+
+/* Component: Hero – with image background, subtle orange */
+.hms-hero {
+ position: relative;
+ background: var(--bg);
+ color: var(--text);
+ overflow: hidden;
+}
+.hms-hero-bg {
+ position: absolute;
+ inset: 0;
+ background-size: cover;
+ background-position: center;
+ opacity: 0.35;
+}
+.hms-hero-overlay {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, rgba(19,19,19,0.6) 0%, rgba(19,19,19,0.85) 50%, var(--bg) 100%);
+}
+
+/* Component: Card */
+.hms-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ transition: box-shadow var(--transition-base), border-color var(--transition-base);
+}
+.hms-card:hover {
+ box-shadow: var(--shadow-lg);
+ border-color: var(--secondary);
+}
+
+/* Component: Button – orange only on primary, subtle */
+.hms-btn {
+ display: inline-flex; align-items: center; justify-content: center;
+ gap: var(--space-sm);
+ font-family: var(--font-sans); font-weight: 600;
+ border-radius: var(--radius-md);
+ transition: all var(--transition-fast);
+ cursor: pointer; border: none; text-decoration: none;
+ white-space: nowrap;
+}
+.hms-btn-primary {
+ background: var(--color-accent); color: white;
+ padding: var(--space-md) var(--space-xl);
+ box-shadow: 0 0 0 0 rgba(236, 105, 37, 0);
+}
+.hms-btn-primary:hover {
+ background: var(--color-accent-hover);
+ box-shadow: 0 0 12px 2px rgba(236, 105, 37, 0.3);
+ transform: translateY(-1px);
+}
+.hms-btn-primary:active { transform: translateY(0); box-shadow: 0 0 6px 0px rgba(236, 105, 37, 0.2); }
+.hms-btn-primary:disabled { background: var(--secondary); cursor: not-allowed; box-shadow: none; transform: none; }
+
+.hms-btn-secondary {
+ background: var(--surface); color: var(--text-muted);
+ border: 1px solid var(--border-strong);
+ padding: var(--space-md) var(--space-xl);
+}
+.hms-btn-secondary:hover {
+ background: var(--row);
+ border-color: var(--color-accent);
+ color: var(--text);
+ box-shadow: 0 0 8px 0 rgba(236, 105, 37, 0.1);
+}
+.hms-btn-secondary:active { background: var(--surface); box-shadow: none; }
+
+.hms-btn-ghost {
+ background: transparent; color: var(--text-muted);
+ padding: var(--space-sm) var(--space-md);
+}
+.hms-btn-ghost:hover { background: var(--surface); color: var(--color-accent); border-color: var(--color-accent-border); }
+
+/* Component: Nav Button (header menu items) */
+.hms-nav-btn {
+ position: relative;
+ transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast) !important;
+}
+.hms-nav-btn:hover {
+ color: var(--color-accent) !important;
+ background: var(--color-accent-light) !important;
+}
+.hms-nav-btn:hover::after {
+ content: '';
+ position: absolute;
+ bottom: 2px; left: 50%;
+ transform: translateX(-50%);
+ width: 60%; height: 2px;
+ background: var(--color-accent);
+ border-radius: var(--radius-full);
+}
+.hms-nav-btn[aria-current="page"]:hover {
+ color: var(--color-accent) !important;
+ background: var(--color-accent-light) !important;
+}
+
+/* Component: Badge – subtle orange */
+.hms-badge {
+ display: inline-flex; align-items: center;
+ padding: 2px var(--space-sm);
+ border-radius: var(--radius-full);
+ font-size: var(--text-xs); font-weight: 600;
+}
+.hms-badge-primary {
+ background: rgba(236, 105, 37, 0.12);
+ color: var(--color-accent);
+ border: 1px solid rgba(236, 105, 37, 0.2);
+}
+.hms-badge-gray { background: var(--surface); color: var(--text-muted); }
+.hms-badge-success { background: var(--color-success-bg); color: var(--color-success); }
+.hms-badge-error { background: var(--color-error-bg); color: var(--color-error); }
+
+/* Component: Input */
+.hms-input {
+ width: 100%;
+ padding: var(--space-md) var(--space-lg);
+ border: 1px solid var(--border-strong);
+ border-radius: var(--radius-md);
+ font-size: var(--text-base);
+ background: var(--surface); color: var(--text);
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+}
+.hms-input::placeholder { color: var(--secondary); }
+.hms-input:focus {
+ outline: none;
+ border-color: var(--color-accent);
+ box-shadow: 0 0 0 2px rgba(236, 105, 37, 0.1);
+}
+.hms-input:disabled { background: var(--row); color: var(--secondary); cursor: not-allowed; }
+.hms-input-error { border-color: var(--color-error); }
+.hms-input-error:focus { box-shadow: 0 0 0 2px rgba(248, 113, 113, 0.1); }
+select.hms-input { background: var(--surface); color: var(--text); }
+select.hms-input option { background: var(--panel); color: var(--text); }
+
+/* Component: Loading Skeleton */
+.hms-skeleton {
+ background: linear-gradient(90deg, var(--surface) 25%, var(--row) 50%, var(--surface) 75%);
+ background-size: 200% 100%;
+ animation: skeleton-shimmer 1.5s infinite;
+ border-radius: var(--radius-md);
+}
+@keyframes skeleton-shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
+
+/* Component: Spinner */
+.hms-spinner {
+ width: 24px; height: 24px;
+ border: 3px solid var(--surface);
+ border-top-color: var(--color-accent);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+
+/* Component: Service Icon Circle – subtle, no large orange area */
+.hms-icon-circle {
+ width: 48px; height: 48px;
+ border-radius: var(--radius-md);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.25rem; flex-shrink: 0;
+}
+
+/* Component: Speaker Grid */
+.hms-speaker-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: var(--space-md);
+}
+.hms-speaker-item {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ text-align: center;
+ transition: all var(--transition-base);
+}
+.hms-speaker-item:hover {
+ border-color: var(--color-accent-border);
+ background: var(--surface);
+}
+.hms-speaker-icon-wrap { display: inline-flex; align-items: center; justify-content: center; margin: 0 auto var(--space-sm); }
+.hms-speaker-icon-wrap svg { width: 48px; height: 48px; color: var(--text-muted); transition: color var(--transition-base); }
+.hms-speaker-item:hover .hms-speaker-icon-wrap svg { color: var(--color-accent); }
+.hms-speaker-name {
+ font-size: var(--text-sm);
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 2px;
+}
+.hms-speaker-type {
+ font-size: var(--text-xs);
+ color: var(--secondary);
+}
+
+/* Component: Footer */
+.hms-footer {
+ background: var(--bg);
+ color: var(--text-muted);
+ border-top: 1px solid var(--border);
+}
+
+/* Gallery Grid */
+.hms-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: var(--space-lg);
+}
+
+/* Equipment Card */
+.hms-eq-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ transition: all var(--transition-base);
+ cursor: pointer;
+}
+.hms-eq-card:hover {
+ border-color: var(--color-accent-border);
+ box-shadow: var(--shadow-lg);
+ transform: translateY(-2px);
+}
+
+/* Filter Chip */
+.hms-chip {
+ display: inline-flex; align-items: center; gap: var(--space-xs);
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-full);
+ font-size: var(--text-sm); font-weight: 500;
+ cursor: pointer;
+ border: 1px solid var(--border-strong);
+ background: var(--surface); color: var(--text-muted);
+ transition: all var(--transition-fast);
+}
+.hms-chip:hover { border-color: var(--secondary); }
+.hms-chip.active {
+ background: var(--color-accent); color: white;
+ border-color: var(--color-accent);
+}
+
+/* Section divider – subtle */
+.hms-section-divider {
+ height: 1px;
+ background: linear-gradient(90deg, transparent 0%, var(--border) 50%, transparent 100%);
+}
+
+/* Mobile Menu Animation */
+.mobile-menu-enter-active, .mobile-menu-leave-active { transition: all var(--transition-base); }
+.mobile-menu-enter-from, .mobile-menu-leave-to { opacity: 0; transform: translateY(-10px); }
+
+/* Fade Transition */
+.fade-enter-active, .fade-leave-active { transition: opacity var(--transition-base); }
+.fade-enter-from, .fade-leave-to { opacity: 0; }
+
+/* Link colors */
+a { color: var(--color-accent); }
+a:hover { color: var(--color-accent-hover); }
+
+/* Responsive */
+@media (max-width: 768px) {
+ :root { --header-height: 3.5rem; }
+ .hms-speaker-grid { grid-template-columns: repeat(2, 1fr); }
+}
diff --git a/frontend/assets/logo.png b/frontend/assets/logo.png
new file mode 100644
index 0000000..1a94cd9
Binary files /dev/null and b/frontend/assets/logo.png differ
diff --git a/frontend/assets/logo.svg b/frontend/assets/logo.svg
new file mode 100644
index 0000000..e824aaa
--- /dev/null
+++ b/frontend/assets/logo.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/components/AppFooter.vue b/frontend/components/AppFooter.vue
index 92d42bd..eeb4239 100644
--- a/frontend/components/AppFooter.vue
+++ b/frontend/components/AppFooter.vue
@@ -1,105 +1,52 @@
-