Stress Index: Analytics e Sport rifatti sulle viste reali
Le due sezioni erano ferme al prototipo locale, più povero della piattaforma vera. Analytics ha ora l'intervallo di date, i KPI con la nota sotto il valore, la distribuzione dello score a quattro serie per fascia, le classifiche per recupero medio col numero di misurazioni e il confronto segmenti con selettore di dimensione. Sport diventa Dashboard, Sessioni con filtri atleta/periodo/sport e schede atleta con carico e tendenza. I numeri non sono più costanti scritte a mano: si calcolano dai dati nel periodo scelto. La logica sta in lib/stress-index/analytics.ts e sport.ts, coperta da test — che hanno subito trovato uno slittamento delle settimane dovuto alle date costruite in ora locale invece che in UTC. Le sessioni hanno ora ora di inizio, HR max, RPE e durata al secondo, che il prototipo non prevedeva. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,62 +1,22 @@
|
||||
---
|
||||
// Analytics di studio: numeri aggregati sul periodo. Il selettore di periodo è ancora
|
||||
// dimostrativo (i dati sorgente sono un'istantanea sola), come nel prototipo.
|
||||
// Analytics di studio. La pagina fornisce i dati e l'intervallo iniziale; il resto — filtri,
|
||||
// aggregazioni e grafici — è nell'isola, perché ogni numero dipende dai controlli.
|
||||
import StressIndex from '../../../layouts/StressIndex.astro';
|
||||
import Card from '../../../components/stress-index/Card.astro';
|
||||
import Kpi from '../../../components/stress-index/Kpi.astro';
|
||||
import StatList from '../../../components/stress-index/StatList.astro';
|
||||
import DataChart from '../../../components/stress-index/charts/DataChart';
|
||||
import { studioKpis, scoreDistribution, segments, topPerformers, criticalClients } from '../../../lib/stress-index/data';
|
||||
import AnalyticsView from '../../../components/stress-index/AnalyticsView';
|
||||
import { clients, measurementsByClient, alerts } from '../../../lib/stress-index/data';
|
||||
export const prerender = false;
|
||||
|
||||
// Mese corrente dei dati dimostrativi.
|
||||
const DA = '2026-07-01';
|
||||
const A = '2026-07-31';
|
||||
---
|
||||
<StressIndex title="Analytics" crumbs={['Analytics']}>
|
||||
<div class="si-head si-head--row">
|
||||
<div>
|
||||
<h1>Analytics</h1>
|
||||
<p>Andamento dello studio nel periodo selezionato</p>
|
||||
</div>
|
||||
<label class="si-field si-field--inline">
|
||||
<span class="si-visually-hidden">Intervallo</span>
|
||||
<select>
|
||||
<option value="7">Ultimi 7 giorni</option>
|
||||
<option value="30" selected>Ultimi 30 giorni</option>
|
||||
<option value="90">Ultimi 90 giorni</option>
|
||||
<option value="365">Ultimo anno</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="si-grid si-grid--kpi">
|
||||
<Kpi label="Clienti attivi" value={studioKpis.totalClients} />
|
||||
<Kpi label="Misurazioni nel periodo" value={studioKpis.measurementsThisMonth} />
|
||||
<Kpi label="Alert attivi" value={studioKpis.activeAlerts} />
|
||||
<Kpi label="Aderenza" value={`${studioKpis.retentionRate}%`} />
|
||||
</div>
|
||||
|
||||
<div class="si-stack si-mt">
|
||||
<Card title="Distribuzione dello score dei clienti">
|
||||
<DataChart client:visible type="bar" data={scoreDistribution} xKey="range" series={[{ key: 'count', label: 'Clienti' }]} />
|
||||
</Card>
|
||||
|
||||
<Card title="Confronto fra segmenti">
|
||||
<div class="si-grid si-grid--halves">
|
||||
{segments.map((s) => (
|
||||
<div class="si-segment">
|
||||
<p class="si-segment__label">{s.label}</p>
|
||||
<p class="si-kpi__value">{s.avgStress}</p>
|
||||
<p class="si-muted si-small">stress medio · {s.count} {s.count === 1 ? 'cliente' : 'clienti'}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div class="si-grid si-grid--halves">
|
||||
<Card title="Andamento migliore">
|
||||
<StatList items={topPerformers.map((c) => ({ label: c.name, value: c.stress }))} />
|
||||
</Card>
|
||||
<Card title="Clienti critici">
|
||||
<StatList items={criticalClients.map((c) => ({ label: c.name, value: c.stress }))} />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<AnalyticsView
|
||||
client:load
|
||||
clients={clients}
|
||||
measurementsByClient={measurementsByClient}
|
||||
alerts={alerts}
|
||||
from={DA}
|
||||
to={A}
|
||||
/>
|
||||
</StressIndex>
|
||||
|
||||
@@ -1,121 +1,13 @@
|
||||
---
|
||||
// Modulo Sport: carico di lavoro e sessioni degli atleti seguiti. Le tre viste sono già
|
||||
// tutte renderizzate: il cambio scheda è solo un mostra/nascondi, non serve un'isola.
|
||||
// Modulo Sport. La pagina passa i dati; filtri, schede e aggregazioni stanno nell'isola.
|
||||
import StressIndex from '../../../../layouts/StressIndex.astro';
|
||||
import Card from '../../../../components/stress-index/Card.astro';
|
||||
import Kpi from '../../../../components/stress-index/Kpi.astro';
|
||||
import DataChart from '../../../../components/stress-index/charts/DataChart';
|
||||
import { sessions, athletes, sessionsPerWeek } from '../../../../lib/stress-index/data';
|
||||
import SportView from '../../../../components/stress-index/SportView';
|
||||
import { sessions, athletes } from '../../../../lib/stress-index/data';
|
||||
export const prerender = false;
|
||||
|
||||
const trimpMedio = Math.round(sessions.reduce((s, x) => s + x.trimp, 0) / sessions.length);
|
||||
const sessioniSettimana = sessionsPerWeek[sessionsPerWeek.length - 1]?.count ?? 0;
|
||||
const nomeAtleta = (id: string) => athletes.find((a) => a.clientId === id)?.name ?? id;
|
||||
const TABS = [
|
||||
{ key: 'dashboard', label: 'Dashboard' },
|
||||
{ key: 'sessioni', label: 'Sessioni' },
|
||||
{ key: 'atleti', label: 'Atleti' },
|
||||
];
|
||||
// Ultimo giorno coperto dai dati dimostrativi.
|
||||
const RIFERIMENTO = '2026-07-12';
|
||||
---
|
||||
<StressIndex title="Sport" crumbs={['Sport']}>
|
||||
<div class="si-head">
|
||||
<h1>Sport</h1>
|
||||
<p>Sessioni, carico e monitoraggio degli atleti</p>
|
||||
</div>
|
||||
|
||||
<div class="si-tabs" role="tablist" id="si-sport-tabs">
|
||||
{TABS.map((t, i) => (
|
||||
<button type="button" role="tab" class="si-tabs__btn" data-tab={t.key} aria-selected={i === 0 ? 'true' : 'false'}>{t.label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section data-panel="dashboard" class="si-stack">
|
||||
<div class="si-grid si-grid--kpi">
|
||||
<Kpi label="Sessioni totali" value={sessions.length} />
|
||||
<Kpi label="Sessioni questa settimana" value={sessioniSettimana} />
|
||||
<Kpi label="TRIMP medio" value={trimpMedio} />
|
||||
<Kpi label="Atleti attivi" value={athletes.length} />
|
||||
</div>
|
||||
|
||||
<Card title="Sessioni per settimana">
|
||||
<DataChart client:visible type="bar" data={sessionsPerWeek} xKey="week" series={[{ key: 'count', label: 'Sessioni' }]} />
|
||||
</Card>
|
||||
|
||||
<Card title="Ultime sessioni">
|
||||
<div class="si-scroll-x">
|
||||
<table class="si-table">
|
||||
<thead><tr><th>Atleta</th><th>Data</th><th>Sport</th><th>Durata</th><th>TRIMP</th><th>HR medio</th><th>DFA A1</th></tr></thead>
|
||||
<tbody>
|
||||
{sessions.map((s) => (
|
||||
<tr>
|
||||
<td><strong>{nomeAtleta(s.athleteId)}</strong></td>
|
||||
<td class="si-table__num">{s.date}</td>
|
||||
<td>{s.type}</td>
|
||||
<td class="si-table__num">{s.durationMin} min</td>
|
||||
<td class="si-table__num">{s.trimp}</td>
|
||||
<td class="si-table__num">{s.hrAvg}</td>
|
||||
<td class="si-table__num">{s.dfaA1}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section data-panel="sessioni" hidden>
|
||||
<Card title="Tutte le sessioni">
|
||||
<div class="si-scroll-x">
|
||||
<table class="si-table">
|
||||
<thead><tr><th>Atleta</th><th>Data</th><th>Sport</th><th>Durata</th><th>Carico</th><th>TRIMP</th><th>HR medio</th><th>DFA A1</th></tr></thead>
|
||||
<tbody>
|
||||
{sessions.map((s) => (
|
||||
<tr>
|
||||
<td><strong>{nomeAtleta(s.athleteId)}</strong></td>
|
||||
<td class="si-table__num">{s.date}</td>
|
||||
<td>{s.type}</td>
|
||||
<td class="si-table__num">{s.durationMin} min</td>
|
||||
<td class="si-table__num">{s.load}</td>
|
||||
<td class="si-table__num">{s.trimp}</td>
|
||||
<td class="si-table__num">{s.hrAvg}</td>
|
||||
<td class="si-table__num">{s.dfaA1}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section data-panel="atleti" hidden>
|
||||
<Card title="Atleti monitorati">
|
||||
<table class="si-table">
|
||||
<thead><tr><th>Atleta</th><th>Sport</th><th>Carico settimanale</th></tr></thead>
|
||||
<tbody>
|
||||
{athletes.map((a) => (
|
||||
<tr>
|
||||
<td><a href={`/piattaforme/stress-index/clienti/${a.clientId}`}>{a.name}</a></td>
|
||||
<td>{a.sport}</td>
|
||||
<td class="si-table__num">{a.weeklyLoad}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
</section>
|
||||
<SportView client:load sessions={sessions} athletes={athletes} riferimento={RIFERIMENTO} />
|
||||
</StressIndex>
|
||||
|
||||
<script>
|
||||
// Schede: un solo pannello visibile alla volta, selezione riflessa su aria-selected.
|
||||
const tabs = document.getElementById('si-sport-tabs');
|
||||
if (tabs) {
|
||||
const buttons = Array.from(tabs.querySelectorAll<HTMLButtonElement>('.si-tabs__btn'));
|
||||
const panels = Array.from(document.querySelectorAll<HTMLElement>('[data-panel]'));
|
||||
for (const btn of buttons) {
|
||||
btn.addEventListener('click', () => {
|
||||
for (const b of buttons) b.setAttribute('aria-selected', String(b === btn));
|
||||
for (const p of panels) p.hidden = p.dataset.panel !== btn.dataset.tab;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PageHero from '../components/PageHero.astro';
|
||||
import T from '../components/content/T.astro';
|
||||
import { t, resolveContent } from '../lib/content';
|
||||
// Variante di test di /promo-eventi ristrutturata sui pattern del sito: banner PageHero
|
||||
// a tutta larghezza, sezioni .section/.container su sfondo bianco, bottoni .btn globali,
|
||||
// font e palette dal design system. Contenuti identici (stessi tag `promo.*`). Gli stili
|
||||
// custom (griglia card, accordion FAQ) sono namespaced sotto `.promo2`.
|
||||
const BSPORT_URL = 'https://backoffice.bsport.io/login/signup?membership=3428';
|
||||
export const prerender = false;
|
||||
|
||||
const cards = [
|
||||
{ id: 1, features: 4, featured: false },
|
||||
{ id: 2, features: 4, featured: true },
|
||||
{ id: 3, features: 3, featured: false },
|
||||
{ id: 4, features: 3, featured: false },
|
||||
];
|
||||
const faqs = [1, 2, 3, 4, 5, 6];
|
||||
const wa = t('global.whatsapp');
|
||||
---
|
||||
<Base title="Promo ed eventi — InsanityLab" description="Edizione apertura: pacchetti a 5 incontri per One to One, Small Group, Reformer e Holistic Class. Acquisto online, pagamento sicuro via Stripe.">
|
||||
<PageHero title={t('promo.pagehero.title')} eyebrow={t('promo.hero.badge')} />
|
||||
|
||||
<div class="promo2">
|
||||
|
||||
<!-- Claim principale -->
|
||||
<section class="section claim-section">
|
||||
<div class="claim-wrap">
|
||||
<h2 class="claim">
|
||||
<span class="l1"><T tag="promo.hero.title1" as="span" /></span>
|
||||
<span class="l2"><em><T tag="promo.hero.accent" as="span" /></em> <T tag="promo.hero.title2" as="span" /></span>
|
||||
</h2>
|
||||
<T tag="promo.hero.desc" as="p" class="claim-desc" />
|
||||
<a class="btn" href="#proposte"><T tag="promo.hero.cta" as="span" /></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Lancio + perché 5 -->
|
||||
<section class="section section--alt">
|
||||
<div class="container launch">
|
||||
<T tag="promo.launch.badge" as="p" class="eyebrow" />
|
||||
<h2><T tag="promo.launch.title" as="span" /></h2>
|
||||
<p class="launch-accent"><T tag="promo.launch.accent" as="span" /></p>
|
||||
<T tag="promo.launch.desc" as="p" />
|
||||
<T tag="promo.why5.text" as="p" class="why5" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pacchetti -->
|
||||
<section class="section" id="proposte">
|
||||
<div class="container">
|
||||
<div class="cards">
|
||||
{cards.map((c) => (
|
||||
<div class:list={['card', c.featured && 'featured']}>
|
||||
{c.featured && <T tag={`promo.card.${c.id}.badge`} as="div" class="featured-badge" />}
|
||||
<T tag={`promo.card.${c.id}.eyebrow`} as="div" class="eyebrow" />
|
||||
<T tag={`promo.card.${c.id}.title`} as="div" class="ptitle" />
|
||||
<div class="price-row">
|
||||
<T tag={`promo.card.${c.id}.price`} as="div" class="price" />
|
||||
<T tag={`promo.card.${c.id}.price-old`} as="div" class="price-old" />
|
||||
</div>
|
||||
<T tag={`promo.card.${c.id}.unit`} as="div" class="unit" />
|
||||
<ul>
|
||||
{Array.from({ length: c.features }, (_, i) => i + 1).map((m) => (
|
||||
<T tag={`promo.card.${c.id}.feature.${m}`} as="li" />
|
||||
))}
|
||||
</ul>
|
||||
<a class:list={['btn', c.featured ? 'btn--dark' : 'btn--light', 'buy']} href={BSPORT_URL} target="_blank" rel="noopener"><T tag="promo.buy-label" as="span" /></a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<T tag="promo.validity-note" as="p" class="validity-note" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section class="section section--alt">
|
||||
<div class="container faq-wrap">
|
||||
<T tag="promo.faq.title" as="h2" class="faq-title" />
|
||||
{faqs.map((n) => {
|
||||
const a = resolveContent(`promo.faq.${n}.a`);
|
||||
return (
|
||||
<div class="faq-block">
|
||||
<div class="faq-q"><T tag={`promo.faq.${n}.q`} as="p" /><div class="faq-toggle">+</div></div>
|
||||
<p class:list={['faq-a', a.classes]} data-tag={`promo.faq.${n}.a`} data-guid={a.guid || undefined} set:html={a.value.replaceAll('{{whatsapp}}', wa)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<script>
|
||||
// Accordion FAQ: click (o Invio/Spazio) sul quesito apre/chiude la risposta.
|
||||
document.querySelectorAll('.promo2 .faq-block .faq-q').forEach((q) => {
|
||||
const block = q.closest('.faq-block');
|
||||
const toggle = q.querySelector('.faq-toggle');
|
||||
q.setAttribute('role', 'button');
|
||||
q.setAttribute('tabindex', '0');
|
||||
q.setAttribute('aria-expanded', 'false');
|
||||
const set = (open: boolean) => {
|
||||
block?.classList.toggle('is-open', open);
|
||||
q.setAttribute('aria-expanded', String(open));
|
||||
if (toggle) toggle.textContent = open ? '−' : '+';
|
||||
};
|
||||
q.addEventListener('click', () => set(!block?.classList.contains('is-open')));
|
||||
q.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set(!block?.classList.contains('is-open')); }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- is:global perché i testi editabili sono renderizzati dal componente <T> (fuori dallo
|
||||
scope della pagina). Tutto è prefissato `.promo2`; la struttura usa le classi globali
|
||||
del sito (.section, .container, .btn, .eyebrow, .section-heading). -->
|
||||
<style is:global>
|
||||
/* Claim principale */
|
||||
.promo2 .claim-section { padding-block: 72px; text-align: center; }
|
||||
.promo2 .claim-wrap { max-width: 820px; margin: 0 auto; padding-inline: 20px; }
|
||||
.promo2 .claim { font-family: var(--font-heading); font-weight: 500; letter-spacing: .02em;
|
||||
color: var(--c-heading); line-height: 1.18; font-size: clamp(2.1rem, 4.5vw, 3.1rem); margin: 0 0 22px; }
|
||||
.promo2 .claim .l1, .promo2 .claim .l2 { display: block; }
|
||||
.promo2 .claim em { font-style: italic; color: var(--c-accent-dark); }
|
||||
.promo2 .claim-desc { max-width: 500px; margin: 0 auto 30px; font-size: 1.05rem; color: var(--c-text); }
|
||||
|
||||
/* Lancio — banda evidenziata */
|
||||
.promo2 .launch { max-width: 760px; text-align: center; }
|
||||
.promo2 .launch .eyebrow { display: block; margin: 0 0 14px; }
|
||||
.promo2 .launch h2 { margin: 0 0 6px; }
|
||||
.promo2 .launch-accent { font-family: var(--font-heading); font-style: italic; font-size: 1.2rem; color: var(--c-accent-dark); margin: 0 0 22px; }
|
||||
.promo2 .launch > p { color: var(--c-text); margin-inline: auto; max-width: 620px; }
|
||||
.promo2 .why5 { font-family: var(--font-heading); font-style: italic; font-size: 1.25rem; line-height: 1.65;
|
||||
color: var(--c-heading); margin: 32px auto 0; max-width: 600px; padding-top: 28px; border-top: 2px solid var(--c-accent); }
|
||||
|
||||
/* Pacchetti */
|
||||
.promo2 .cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; align-items: stretch; }
|
||||
.promo2 .card { background: var(--c-bg); border: 1px solid #e7e0d6; border-radius: 6px; padding: 34px 30px; position: relative; display: flex; flex-direction: column; transition: box-shadow .25s, transform .25s; }
|
||||
.promo2 .card:hover { box-shadow: 0 14px 40px rgba(58,41,41,.10); transform: translateY(-3px); }
|
||||
.promo2 .card.featured { border: 1px solid var(--c-accent); box-shadow: 0 10px 34px rgba(58,41,41,.08); }
|
||||
.promo2 .featured-badge {
|
||||
position: absolute; top: -13px; left: 50%; transform: translateX(-50%); background: var(--c-accent);
|
||||
color: #fff; font-family: var(--font-heading); font-size: 10.5px; font-weight: 600; letter-spacing: .12em;
|
||||
text-transform: uppercase; padding: 6px 16px; border-radius: 999px; white-space: nowrap;
|
||||
}
|
||||
.promo2 .card .eyebrow { margin: 0 0 8px; font-size: .68rem; }
|
||||
.promo2 .card .ptitle { font-family: var(--font-heading); font-size: 1.4rem; font-weight: 500; color: var(--c-heading); letter-spacing: .03em; margin: 0 0 18px; }
|
||||
.promo2 .card .price-row { display: flex; align-items: baseline; gap: 10px; margin: 0 0 3px; }
|
||||
.promo2 .card .price { font-family: var(--font-heading); font-size: 2.4rem; font-weight: 600; color: var(--c-heading); line-height: 1; }
|
||||
.promo2 .card .price-old { font-size: 1.05rem; color: var(--c-text-light); text-decoration: line-through; }
|
||||
.promo2 .card .unit { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--c-text-light); margin: 0 0 22px; }
|
||||
.promo2 .card ul { list-style: none; padding: 22px 0 0; margin: 0 0 26px; border-top: 1px solid #f0eae1; display: flex; flex-direction: column; gap: 11px; }
|
||||
.promo2 .card li { font-size: .9rem; line-height: 1.5; color: var(--c-text); display: flex; gap: 10px; }
|
||||
.promo2 .card li::before { content: "—"; color: var(--c-accent); font-weight: 700; flex-shrink: 0; }
|
||||
.promo2 .card .buy { margin-top: auto; justify-content: center; }
|
||||
.promo2 .validity-note { text-align: center; font-size: .82rem; color: var(--c-text-light); margin: 34px auto 0; max-width: 560px; }
|
||||
|
||||
/* FAQ */
|
||||
.promo2 .faq-wrap { max-width: 760px; }
|
||||
.promo2 .faq-title { text-align: center; text-transform: uppercase; letter-spacing: .1em; font-size: clamp(1.5rem, 3vw, 2rem); margin: 0 0 44px; }
|
||||
.promo2 .faq-block { border-top: 1px solid #e4ded6; padding: 20px 4px; transition: padding .2s; }
|
||||
.promo2 .faq-block:last-child { border-bottom: 1px solid #e4ded6; }
|
||||
.promo2 .faq-q { display: flex; justify-content: space-between; align-items: center; gap: 24px; cursor: pointer; }
|
||||
.promo2 .faq-q p { margin: 0; font-family: var(--font-heading); font-size: 1rem; font-weight: 500; color: var(--c-heading); letter-spacing: .02em; }
|
||||
.promo2 .faq-q:focus-visible { outline: 2px solid var(--c-accent-dark); outline-offset: 4px; }
|
||||
.promo2 .faq-toggle {
|
||||
width: 32px; height: 32px; border-radius: 50%; border: 1px solid #d8cfc2; display: flex;
|
||||
align-items: center; justify-content: center; flex-shrink: 0; font-size: 17px; color: var(--c-heading);
|
||||
transition: background .2s ease, color .2s ease, border-color .2s ease;
|
||||
}
|
||||
.promo2 .faq-block.is-open .faq-toggle { background: var(--c-accent); color: #fff; border-color: var(--c-accent); }
|
||||
.promo2 .faq-a { display: none; margin: 14px 0 0; font-size: .94rem; line-height: 1.75; color: var(--c-text); max-width: 640px; }
|
||||
.promo2 .faq-block.is-open .faq-a { display: block; }
|
||||
.promo2 .faq-a .hl { color: var(--c-accent-dark); font-weight: 600; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.promo2 .cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user