Linus 2026-11-06: everything public lives on imagesbyolofsson.se (Cleura), the customer subdomain is only for ordering and the logged-in portal; photos are sold as posters instead of being downloadable. - Portfolio: download menu (S/M/L), the original password dialog and /download/original are gone; the lightbox has "Köp som poster" -> /posters instead. - about.astro and contact.astro removed: Caddy on Cleura sends /about, /contact and /posters to the portal, so the About text and collaborations are edited once in admin. They stay in sitemap-0.xml (PORTAL_PAGES); the sitemap index lists /posters/sitemap.xml. - Menu and 404 page link Posters; privacy, robots.txt and llms.txt no longer mention password downloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
429 lines
20 KiB
Plaintext
429 lines
20 KiB
Plaintext
---
|
||
// src/pages/portfolio.astro
|
||
import MainLayout from '../layouts/MainLayout.astro';
|
||
import { Image, getImage } from 'astro:assets';
|
||
import exifr from 'exifr';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import WaveBackground from '../components/WaveBackground.astro';
|
||
import { business, genreList } from '../data/business';
|
||
import { portfolioMeta } from '../data/portfolio-meta';
|
||
|
||
// SERVER-SIDE LOGIC (byggtid)
|
||
|
||
// 1. Hämta alla galleribilder.
|
||
// -s (800px) och -m (1600px) i mappen är gamla nedladdningsvarianter med EXIF kvar.
|
||
// De importeras INTE längre (då hamnar de inte i dist/); nedladdningsstorlekarna
|
||
// tas i stället fram av Astros bildtjänst nedan, vilket också rensar bort EXIF
|
||
// (GPS, kamerans serienummer, Artist m.m.).
|
||
const imageFiles = import.meta.glob(
|
||
[
|
||
'../assets/images/portfolio/*.{jpeg,jpg,png,webp,JPG,JPEG,PNG}',
|
||
'!../assets/images/portfolio/*-{s,m}.{jpeg,jpg,png,webp,JPG,JPEG,PNG}',
|
||
],
|
||
{ eager: true }
|
||
);
|
||
|
||
// OBS: läs aldrig .src/.width/.height direkt på de importerade bilderna här.
|
||
// Då räknar Astro originalet som "använt" och kopierar originalfilen (med EXIF)
|
||
// till dist/. Gå alltid via <Image>/getImage.
|
||
|
||
function formatShutterSpeed(exposureTime: number) {
|
||
if (!exposureTime) return null;
|
||
if (exposureTime >= 1) return exposureTime.toString();
|
||
const fraction = Math.round(1 / exposureTime);
|
||
return `1/${fraction}`;
|
||
}
|
||
|
||
// Datum ur filnamnet: "immich-<id>-20260905-..." eller "20250510-...".
|
||
function dateFromFilename(name: string): Date | null {
|
||
const m = name.match(/(?:^|-)((?:19|20)\d{2})(\d{2})(\d{2})-/);
|
||
if (!m) return null;
|
||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
|
||
const widthOf = (descriptor: string) => parseInt(descriptor, 10) || 0;
|
||
|
||
const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([relativePath, file]: [string, any]) => {
|
||
let exifData: any = {};
|
||
let dateTaken: Date | null = null;
|
||
const absolutePath = path.resolve('./src/pages', relativePath);
|
||
const fileName = path.basename(relativePath);
|
||
// Stabil nyckel = filnamn utan ändelse. Används för alt-texter (portfolio-meta.ts) och statistik.
|
||
const key = fileName.replace(/\.[^.]+$/, '');
|
||
|
||
try {
|
||
const fileBuffer = fs.readFileSync(absolutePath);
|
||
exifData = await exifr.parse(fileBuffer, [
|
||
'ISO', 'Model', 'FNumber', 'ExposureTime', 'FocalLength', 'DateTimeOriginal', 'CreateDate'
|
||
]);
|
||
const exifDate = exifData?.DateTimeOriginal || exifData?.CreateDate;
|
||
if (exifDate) dateTaken = new Date(exifDate);
|
||
} catch (e) {
|
||
console.error(`Kunde inte läsa EXIF för ${relativePath}`);
|
||
}
|
||
if (!dateTaken || isNaN(dateTaken.getTime())) dateTaken = dateFromFilename(fileName);
|
||
const hasDate = !!dateTaken;
|
||
const dateObj = dateTaken ?? new Date();
|
||
|
||
const year = dateObj.getFullYear().toString();
|
||
const month = dateObj.toLocaleString('sv-SE', { month: 'long' }).toLowerCase();
|
||
const monthIndex = dateObj.getMonth();
|
||
|
||
const fullDate = hasDate
|
||
? dateObj.toLocaleDateString('sv-SE', { day: 'numeric', month: 'long', year: 'numeric' })
|
||
: '';
|
||
|
||
const shutterSpeed = formatShutterSpeed(exifData?.ExposureTime);
|
||
const focalLength = exifData?.FocalLength ? `${Math.round(exifData.FocalLength)}mm` : null;
|
||
const fStop = exifData?.FNumber ? `f/${exifData.FNumber}` : null;
|
||
const iso = exifData?.ISO ? `ISO ${exifData.ISO}` : null;
|
||
|
||
// Alt-text: riktig text från portfolio-meta.ts om den finns, annars en neutral
|
||
// svensk reservtext. Aldrig kameramodell, aldrig "undefined".
|
||
const meta = portfolioMeta[key] ?? {};
|
||
const fallbackAlt = [
|
||
meta.category ? `${meta.category}:` : null,
|
||
`fotografi av ${business.person.name}`,
|
||
fullDate ? `taget ${fullDate}` : null,
|
||
].filter(Boolean).join(' ');
|
||
const alt = meta.alt?.trim() || fallbackAlt.charAt(0).toUpperCase() + fallbackAlt.slice(1);
|
||
|
||
// Lightbox: EXIF-fria JPG:er i 800/1600/3000 px (Astro hoppar över bredder större än originalet).
|
||
// Ingen nedladdning längre (Linus 2026-11-06): bilderna säljs som posters i stället.
|
||
const jpg = await getImage({ src: file.default, format: 'jpg', quality: 88, width: 800, widths: [800, 1600, 3000] });
|
||
const variants = [...jpg.srcSet.values].sort((a, b) => widthOf(a.descriptor ?? '') - widthOf(b.descriptor ?? ''));
|
||
const large = variants[variants.length - 1]?.url ?? jpg.src;
|
||
|
||
const statKey = ('portfolio/' + key.replace(/[^\w\-.]/g, '_')).slice(0, 150);
|
||
|
||
return {
|
||
img: file.default, key, alt, title: meta.title ?? null,
|
||
year, month, monthIndex, fullDate, model: exifData?.Model,
|
||
fStop, shutterSpeed, iso, focalLength,
|
||
large, srcset: jpg.srcSet.attribute,
|
||
statKey,
|
||
};
|
||
}));
|
||
|
||
const uniqueYears = [...new Set(galleryItems.map((item: any) => item.year))].sort().reverse();
|
||
const allMonths = galleryItems.map((item: any) => ({ name: item.month, index: item.monthIndex }));
|
||
const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).values()]
|
||
.sort((a: any, b: any) => a.index - b.index)
|
||
.map((item: any) => item.name);
|
||
---
|
||
|
||
<MainLayout
|
||
title="Portfolio – fotografier av Linus Olofsson | Images by Olofsson"
|
||
description={`Portfolio med fotografier av Linus Olofsson: ${genreList()}. Filtrera på år och månad och öppna bilderna i större format.`}
|
||
path="/portfolio"
|
||
pageType="CollectionPage"
|
||
breadcrumb="Portfolio"
|
||
>
|
||
|
||
<div>
|
||
<WaveBackground opacity="opacity-55" />
|
||
</div>
|
||
|
||
<div class="container mx-auto px-4 py-12">
|
||
|
||
<div class="text-center mb-12">
|
||
<h1 class="text-4xl md:text-6xl font-bold mb-4">Portfolio</h1>
|
||
<p class="text-gray-300 max-w-2xl mx-auto">Ett urval av mina bilder i olika stilar – {genreList()}.
|
||
Vill du boka en fotografering? Läs om <a href="/tjanster" class="text-white underline underline-offset-4 hover:text-gray-300">tjänster och bokning</a>.</p>
|
||
</div>
|
||
|
||
<div class="flex flex-col items-center gap-6 mb-12">
|
||
<div class="flex flex-wrap justify-center gap-2">
|
||
<button type="button" class="filter-btn active" data-filter="all" data-type="year">Alla år</button>
|
||
{uniqueYears.map(year => (
|
||
<button type="button" class="filter-btn" data-filter={year} data-type="year">{year}</button>
|
||
))}
|
||
</div>
|
||
|
||
<div class="flex flex-wrap justify-center gap-2">
|
||
<button type="button" class="filter-btn active" data-filter="all" data-type="month">Alla månader</button>
|
||
{uniqueMonths.map(month => (
|
||
<button type="button" class="filter-btn capitalize" data-filter={month} data-type="month">{month}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<h2 class="sr-only">Bilder</h2>
|
||
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
|
||
{galleryItems.map((item: any, index: number) => (
|
||
<a
|
||
href={item.large}
|
||
class="gallery-item block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
|
||
data-year={item.year}
|
||
data-month={item.month}
|
||
data-src={item.large}
|
||
data-srcset={item.srcset}
|
||
data-key={item.key}
|
||
data-stat={item.statKey}
|
||
data-alt={item.alt}
|
||
data-title={item.title}
|
||
data-model={item.model}
|
||
data-iso={item.iso}
|
||
data-fstop={item.fStop}
|
||
data-shutter={item.shutterSpeed}
|
||
data-focal={item.focalLength}
|
||
data-date={item.fullDate}
|
||
>
|
||
<Image
|
||
src={item.img}
|
||
alt={item.alt}
|
||
width={800}
|
||
widths={[480, 800]}
|
||
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
||
loading={index < 3 ? 'eager' : 'lazy'}
|
||
decoding="async"
|
||
class="w-full h-auto object-cover transform transition duration-700 group-hover:scale-105"
|
||
/>
|
||
|
||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition duration-300 flex flex-col justify-end p-6 pointer-events-none">
|
||
{(item.title || item.fullDate) && <p class="text-white font-bold text-lg">{item.title ?? item.fullDate}</p>}
|
||
<p class="text-xs text-gray-300 uppercase tracking-widest mt-1">Visa bild</p>
|
||
</div>
|
||
</a>
|
||
))}
|
||
</div>
|
||
|
||
<div id="no-results" class="hidden text-center text-gray-400 mt-20">
|
||
<p class="text-xl">Inga bilder hittades för denna tidpunkt.</p>
|
||
<button type="button" id="reset-btn" class="mt-4 text-white underline hover:text-gray-300">Visa alla bilder</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div id="lightbox" class="fixed inset-0 z-50 hidden bg-black/95 backdrop-blur-md flex items-center justify-center opacity-0 transition-opacity duration-300 p-2 md:p-8">
|
||
<div class="bg-neutral-900 w-full max-w-6xl h-[85vh] md:h-[90vh] flex flex-col rounded-xl overflow-hidden shadow-2xl relative border border-white/10">
|
||
|
||
<button type="button" id="lb-close" aria-label="Stäng" class="absolute top-4 right-4 z-50 bg-black/50 hover:bg-black/80 text-white p-2 rounded-full transition-colors">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||
</button>
|
||
|
||
<div class="flex-1 relative bg-black flex items-center justify-center overflow-hidden group">
|
||
<img id="lb-img" src="" alt="" sizes="100vw" class="max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
|
||
<button type="button" id="lb-prev" aria-label="Föregående bild" class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||
</button>
|
||
<button type="button" id="lb-next" aria-label="Nästa bild" class="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="bg-neutral-800 p-4 md:p-6 shrink-0 border-t border-white/5 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||
<div class="text-left">
|
||
<p id="lb-date" class="text-white font-bold text-lg md:text-xl"></p>
|
||
<p id="lb-model" class="text-gray-400 text-sm font-medium tracking-wide mt-1"></p>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-4 md:gap-6">
|
||
<a href="/posters" class="inline-flex items-center gap-2 px-6 py-2.5 bg-neutral-700/80 hover:bg-neutral-600/80 text-gray-200 hover:text-white font-semibold text-xs uppercase tracking-widest rounded-full border border-black/60 shadow-lg shadow-black/30 ring-1 ring-white/5 transition-all duration-200 transform hover:scale-105">
|
||
Köp som poster
|
||
</a>
|
||
<div class="flex flex-wrap gap-3 md:gap-6 text-xs md:text-sm font-mono text-gray-300">
|
||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Brännvidd</span><span id="lb-focal">--</span></div>
|
||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Bländare</span><span id="lb-fstop">--</span></div>
|
||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Slutartid</span><span id="lb-shutter">--</span></div>
|
||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">ISO</span><span id="lb-iso">--</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<script>
|
||
// --- ANONYM STATISTIK ---
|
||
// Räknare per dag och bild i kundportalen: ingen cookie, ingen IP, inget id.
|
||
const STATS_URL = 'https://customer.imagesbyolofsson.se/api/stats/hit';
|
||
// Bildens statistiknyckel räknas ut vid bygget (data-stat) och är densamma som
|
||
// tidigare ("portfolio/<filnamn utan ändelse>"), så historiken fortsätter.
|
||
function statsItem(item?: HTMLElement): string {
|
||
return item?.dataset.stat || 'portfolio/okand';
|
||
}
|
||
function stat(kind: 'view' | 'open', item: string) {
|
||
try {
|
||
const body = new Blob([JSON.stringify({ kind, item })], { type: 'text/plain' });
|
||
navigator.sendBeacon?.(STATS_URL, body);
|
||
} catch { /* statistik får aldrig störa sidan */ }
|
||
}
|
||
stat('view', 'page/portfolio');
|
||
|
||
// --- FILTER LOGIK ---
|
||
// MULTI-select: flera år + månader samtidigt. 'all' = samtligt.
|
||
let selectedYears = new Set(['all']);
|
||
let selectedMonths = new Set(['all']);
|
||
|
||
const items = Array.from(document.querySelectorAll('.gallery-item')) as HTMLElement[];
|
||
const buttons = document.querySelectorAll('.filter-btn');
|
||
const noResults = document.getElementById('no-results');
|
||
const resetBtn = document.getElementById('reset-btn');
|
||
|
||
function filterImages() {
|
||
let visibleCount = 0;
|
||
items.forEach((el) => {
|
||
const itemYear = el.dataset.year;
|
||
const itemMonth = el.dataset.month;
|
||
const matchYear = selectedYears.has('all') || selectedYears.has(itemYear);
|
||
const matchMonth = selectedMonths.has('all') || selectedMonths.has(itemMonth);
|
||
|
||
if (matchYear && matchMonth) {
|
||
el.style.display = 'block';
|
||
el.classList.add('visible-item');
|
||
if(el.style.opacity === '0') {
|
||
el.animate([{ opacity: 0, transform: 'scale(0.95)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 300, easing: 'ease-out' });
|
||
}
|
||
visibleCount++;
|
||
} else {
|
||
el.style.display = 'none';
|
||
el.classList.remove('visible-item');
|
||
}
|
||
});
|
||
if (noResults) noResults.style.display = visibleCount === 0 ? 'block' : 'none';
|
||
}
|
||
|
||
// --- LIGHTBOX LOGIK ---
|
||
const lightbox = document.getElementById('lightbox');
|
||
const lbImg = document.getElementById('lb-img') as HTMLImageElement;
|
||
const lbDate = document.getElementById('lb-date');
|
||
const lbModel = document.getElementById('lb-model');
|
||
const lbFocal = document.getElementById('lb-focal');
|
||
const lbFstop = document.getElementById('lb-fstop');
|
||
const lbShutter = document.getElementById('lb-shutter');
|
||
const lbIso = document.getElementById('lb-iso');
|
||
const closeBtn = document.getElementById('lb-close');
|
||
const nextBtn = document.getElementById('lb-next');
|
||
const prevBtn = document.getElementById('lb-prev');
|
||
|
||
let currentImageIndex = 0;
|
||
let visibleItems: HTMLElement[] = [];
|
||
|
||
function updateVisibleItems() {
|
||
visibleItems = items.filter(item => item.style.display !== 'none');
|
||
}
|
||
|
||
function openLightbox(index: number) {
|
||
updateVisibleItems();
|
||
if (visibleItems.length === 0) return;
|
||
currentImageIndex = index;
|
||
updateLightboxContent();
|
||
|
||
lightbox?.classList.remove('hidden');
|
||
setTimeout(() => { lightbox?.classList.remove('opacity-0'); }, 10);
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeLightbox() {
|
||
lightbox?.classList.add('opacity-0');
|
||
setTimeout(() => {
|
||
lightbox?.classList.add('hidden');
|
||
document.body.style.overflow = '';
|
||
}, 300);
|
||
}
|
||
|
||
function updateLightboxContent() {
|
||
const item = visibleItems[currentImageIndex];
|
||
if (!item) return;
|
||
// srcset först så att webbläsaren väljer 800/1600/3000 px efter skärmen.
|
||
lbImg.srcset = item.dataset.srcset || '';
|
||
lbImg.src = item.dataset.src || '';
|
||
lbImg.alt = item.dataset.alt || '';
|
||
stat('open', statsItem(item));
|
||
|
||
if(lbDate) lbDate.innerText = [item.dataset.title, item.dataset.date].filter(Boolean).join(' – ');
|
||
if(lbModel) lbModel.innerText = item.dataset.model || '';
|
||
if(lbFocal) lbFocal.innerText = item.dataset.focal || '--';
|
||
if(lbFstop) lbFstop.innerText = item.dataset.fstop || '--';
|
||
if(lbShutter) lbShutter.innerText = item.dataset.shutter ? item.dataset.shutter + 's' : '--';
|
||
if(lbIso) lbIso.innerText = item.dataset.iso || '--';
|
||
}
|
||
|
||
function showNext() {
|
||
currentImageIndex = (currentImageIndex + 1) % visibleItems.length;
|
||
updateLightboxContent();
|
||
}
|
||
|
||
function showPrev() {
|
||
currentImageIndex = (currentImageIndex - 1 + visibleItems.length) % visibleItems.length;
|
||
updateLightboxContent();
|
||
}
|
||
|
||
// Event Listeners
|
||
items.forEach((item) => {
|
||
item.classList.add('visible-item');
|
||
item.addEventListener('click', (e) => {
|
||
// Länken pekar på den stora bilden (crawlbar/öppningsbar i ny flik),
|
||
// men ett vanligt klick öppnar lightboxen.
|
||
const me = e as MouseEvent;
|
||
if (me.metaKey || me.ctrlKey || me.shiftKey || me.button === 1) return;
|
||
e.preventDefault();
|
||
updateVisibleItems();
|
||
const index = visibleItems.indexOf(item);
|
||
if (index !== -1) openLightbox(index);
|
||
});
|
||
});
|
||
|
||
closeBtn?.addEventListener('click', closeLightbox);
|
||
nextBtn?.addEventListener('click', (e) => { e.stopPropagation(); showNext(); });
|
||
prevBtn?.addEventListener('click', (e) => { e.stopPropagation(); showPrev(); });
|
||
|
||
lightbox?.addEventListener('click', (e) => {
|
||
if (e.target === lightbox) closeLightbox();
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (lightbox?.classList.contains('hidden')) return;
|
||
if (e.key === 'Escape') closeLightbox();
|
||
if (e.key === 'ArrowRight') showNext();
|
||
if (e.key === 'ArrowLeft') showPrev();
|
||
});
|
||
|
||
// Filter Listeners
|
||
buttons.forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const target = e.target as HTMLElement;
|
||
const type = target.dataset.type;
|
||
const value = target.dataset.filter;
|
||
if (!type || !value) return;
|
||
|
||
const sel = type === 'year' ? selectedYears : selectedMonths;
|
||
if (value === 'all') { sel.clear(); sel.add('all'); }
|
||
else {
|
||
sel.delete('all');
|
||
sel.has(value) ? sel.delete(value) : sel.add(value);
|
||
if (sel.size === 0) sel.add('all');
|
||
}
|
||
document.querySelectorAll(`.filter-btn[data-type="${type}"]`).forEach(b => {
|
||
b.classList.toggle('active', b.dataset.filter === 'all' ? sel.has('all') : sel.has(b.dataset.filter));
|
||
});
|
||
filterImages();
|
||
});
|
||
});
|
||
|
||
resetBtn?.addEventListener('click', () => {
|
||
selectedYears = new Set(['all']);
|
||
selectedMonths = new Set(['all']);
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||
document.querySelector('.filter-btn[data-filter="all"][data-type="year"]')?.classList.add('active');
|
||
document.querySelector('.filter-btn[data-filter="all"][data-type="month"]')?.classList.add('active');
|
||
filterImages();
|
||
});
|
||
</script>
|
||
</MainLayout>
|
||
|
||
<style>
|
||
@reference "../styles/global.css";
|
||
|
||
.filter-btn {
|
||
@apply px-4 py-1.5 rounded-full border border-white/10 bg-neutral-800/50 text-sm text-gray-400 transition hover:bg-white hover:text-black hover:border-white;
|
||
}
|
||
|
||
.filter-btn.active {
|
||
@apply bg-white text-black border-white font-medium shadow-[0_0_15px_rgba(255,255,255,0.3)];
|
||
}
|
||
</style>
|