Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ textarea:focus, .choice:focus-within { outline: 2px solid var(--accent); outline

/* ---------- Footer (appwerk styles.css:175-183) ---------- */
footer.site { background: var(--ink); color: #b8becd; padding: 56px 0 40px; margin-top: 0; font-size: 15px; }
footer.site .wrap { max-width: 1120px; }
/* Header and footer share the 1200px frame of the redesigned pages, so the logo lines up. */
.nav .wrap, footer.site .wrap { max-width: 1200px; }
footer.site .nav-logo { color: #fff; }
footer.site .logo-by { color: #94a3b8; }
.footer-inner { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; flex-wrap: wrap; margin-bottom: 32px; }
Expand Down Expand Up @@ -312,3 +313,25 @@ footer.site .logo-by { color: #94a3b8; }
.chat-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.chat-shot-remove { position: absolute; top: 2px; right: 2px; width: 24px; height: 24px; border-radius: 50%; border: 0; background: rgba(11, 14, 20, .75); color: #fff; cursor: pointer; line-height: 24px; padding: 0; }
@media (max-width: 520px) { .chat-msg { max-width: 100%; } .chat-compose { flex-direction: column; align-items: stretch; } .chat-compose-side { justify-content: flex-end; } }

/* ---------- Consent banner (AdConsent) ---------- */
.consent { position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 60; max-width: 520px; margin: 0 auto; max-height: calc(100vh - 32px); overflow-y: auto; background: #fff; color: var(--ink); border: 1px solid var(--border); border-radius: 20px; box-shadow: 0 30px 70px rgba(15, 23, 42, 0.22); padding: 22px 22px 18px; }
.consent-title { display: block; font-family: var(--font-display); font-size: 1.1rem; margin-bottom: 6px; }
.consent-text { margin: 0 0 14px; font-size: 0.9rem; line-height: 1.55; color: var(--ink-soft); }
.consent-text a { color: var(--accent-hover); }
.consent-opts { display: grid; gap: 8px; margin-bottom: 16px; }
.consent-opt { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; background: var(--tint); border-radius: 12px; padding: 12px 14px; font-size: 0.86rem; line-height: 1.45; cursor: pointer; }
.consent-opt strong { display: block; font-size: 0.92rem; margin-bottom: 2px; }
.consent-opt span span { color: var(--ink-soft); }
.consent-switch { appearance: none; -webkit-appearance: none; flex: none; position: relative; width: 40px; height: 24px; margin: 2px 0 0; border-radius: 99px; background: #cbd5e1; cursor: pointer; transition: background 0.15s ease; }
.consent-switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25); transition: transform 0.15s ease; }
.consent-switch:checked { background: var(--accent); }
.consent-switch:checked::after { transform: translateX(16px); }
.consent-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.consent-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.consent-btn { padding: 11px 14px; border: 1.5px solid var(--ink); border-radius: 12px; background: #fff; color: var(--ink); font: 600 0.9rem var(--font-body); cursor: pointer; }
.consent-btn:hover { background: var(--ink); color: #fff; }
.consent-save { grid-column: 1 / -1; padding: 6px; background: none; border: 0; font: 600 0.86rem var(--font-body); color: var(--accent); cursor: pointer; }
.consent-save:hover { text-decoration: underline; }
.consent-reopen { background: none; border: 0; padding: 0; color: #b8becd; font-size: 0.88rem; cursor: pointer; font-family: inherit; }
.consent-reopen:hover { color: var(--paper); }
40 changes: 40 additions & 0 deletions apps/web/src/app/legal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* Legal pages and the security page, in the look of the home page: head band, the text in one
white card, links to the other documents as pills. Scoped under `.lg`. */

.lg { padding-bottom: 80px; }
.lg .wrap { max-width: 1200px; }
.lg .wrap-narrow { max-width: 820px; }
.lg-band { padding: 40px 0 84px; }
.lg-band h1 { font-size: clamp(2rem, 3.6vw, 2.8rem); letter-spacing: -0.04em; margin: 0 0 12px; }
.lg .lg-lede { font-size: 1.1rem; color: var(--ink-soft); line-height: 1.6; max-width: 40em; margin: 0; }
.lg-body { margin-top: -48px; position: relative; }

.lg .lg-draft { max-width: none; margin: 0 0 16px; border-radius: 14px; font-size: 0.88rem; line-height: 1.55; }
.lg-card { background: #fff; border: 1px solid var(--border); border-radius: 20px; padding: 8px 36px 32px; box-shadow: 0 30px 70px rgba(15, 23, 42, 0.08); }
.lg .lg-card section, .lg .lg-body > section { padding: 0; margin-top: 28px; }
.lg section h2 { font-size: 1.2rem; letter-spacing: -0.02em; margin: 0 0 8px; }
.lg section p { max-width: 68ch; margin: 0 0 10px; line-height: 1.7; }
.lg .lg-updated { margin: 24px 0 12px; font-size: 0.84rem; color: var(--ink-soft); }
.lg-more { display: flex; flex-wrap: wrap; gap: 8px; }
.lg-more a { padding: 7px 14px; border: 1px solid var(--border); border-radius: 99px; background: #fff; font-size: 0.86rem; font-weight: 600; color: var(--ink); text-decoration: none; }
.lg-more a:hover { border-color: #c7d2fe; color: var(--accent-hover); }

/* Security page: the promises as cards over the band, then the text. */
.lg-promises { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: -48px; position: relative; }
.lg-promise { background: #fff; border: 1px solid var(--border); border-radius: 16px; padding: 20px; box-shadow: var(--shadow); }
.lg-promise h3 { font-size: 1rem; line-height: 1.3; margin: 12px 0 6px; }
.lg-promise p { font-size: 0.88rem; color: var(--ink-soft); line-height: 1.55; margin: 0; }
.lg-promise-ico { width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; background: #dcfce7; color: var(--valid); }
.lg-promise-ico .ico { width: 20px; height: 20px; }
.lg .lg-promises + .lg-body { margin-top: 24px; background: #fff; border: 1px solid var(--border); border-radius: 20px; padding: 8px 36px 32px; box-shadow: var(--shadow); }
.lg .lg-table { margin-top: 12px; border-radius: 14px; }

@media (max-width: 900px) {
.lg-promises { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
.lg-promises { grid-template-columns: minmax(0, 1fr); }
.lg .lg-promises + .lg-body { padding: 4px 20px 24px; margin-left: 16px; margin-right: 16px; }
.lg-band { padding: 28px 0 72px; }
.lg-card { padding: 4px 20px 24px; border-radius: 18px; }
}
42 changes: 18 additions & 24 deletions apps/web/src/components/AdConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,37 +58,31 @@ export function AdConsentBanner({ d, locale }: { d: Dict; locale: Locale }) {
};

const option = (key: keyof Consent, title: string, text: string) => (
<label style={{ display: "flex", gap: 10, alignItems: "flex-start", margin: "0 0 10px", fontSize: "0.9rem", cursor: "pointer" }}>
<input type="checkbox" checked={pick[key]} onChange={(e) => setPick({ ...pick, [key]: e.target.checked })} style={{ marginTop: 3 }} />
<label className="consent-opt">
<span>
<strong style={{ display: "block" }}>{title}</strong>
<span style={{ color: "var(--ink-soft)" }}>{text}</span>
<strong>{title}</strong>
<span>{text}</span>
</span>
<input type="checkbox" role="switch" className="consent-switch" checked={pick[key]} onChange={(e) => setPick({ ...pick, [key]: e.target.checked })} />
</label>
);

return (
<div
role="dialog"
aria-label={c.title}
style={{
position: "fixed", left: 16, right: 16, bottom: 16, zIndex: 60, maxWidth: 540, margin: "0 auto",
maxHeight: "calc(100vh - 32px)", overflowY: "auto",
background: "var(--surface)", color: "var(--ink)", border: "1px solid var(--border)",
borderRadius: "var(--radius)", boxShadow: "0 12px 40px rgba(11,14,20,0.18)", padding: "18px 20px",
}}
>
<strong style={{ display: "block", marginBottom: 6 }}>{c.title}</strong>
<p style={{ margin: "0 0 12px", fontSize: "0.92rem", color: "var(--ink-soft)" }}>
<div role="dialog" aria-label={c.title} className="consent">
<strong className="consent-title">{c.title}</strong>
<p className="consent-text">
{c.text}{" "}
<Link href={`/${locale}/privacy`} style={{ color: "inherit" }}>{c.more}</Link>
<Link href={`/${locale}/privacy`}>{c.more}</Link>
</p>
{gtmId && option("stats", c.statsTitle, c.statsText)}
{option("ads", c.adsTitle, c.adsText)}
<div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginTop: 4 }}>
<button className="btn btn-ghost btn-sm" onClick={() => save({ stats: Boolean(gtmId), ads: true })}>{c.allowAll}</button>
<button className="btn btn-ghost btn-sm" onClick={() => save(pick)}>{c.saveChoice}</button>
<button className="btn btn-ghost btn-sm" onClick={() => save({ stats: false, ads: false })}>{c.declineAll}</button>
<div className="consent-opts">
{gtmId && option("stats", c.statsTitle, c.statsText)}
{option("ads", c.adsTitle, c.adsText)}
</div>
{/* Allow and decline look the same on purpose: consent may not be nudged (EDPB 03/2022). */}
<div className="consent-actions">
<button className="consent-btn" onClick={() => save({ stats: Boolean(gtmId), ads: true })}>{c.allowAll}</button>
<button className="consent-btn" onClick={() => save({ stats: false, ads: false })}>{c.declineAll}</button>
<button className="consent-save" onClick={() => save(pick)}>{c.saveChoice}</button>
</div>
</div>
);
Expand All @@ -100,7 +94,7 @@ export function AdConsentLink({ label }: { label: string }) {
<button
type="button"
onClick={reopen}
style={{ background: "none", border: 0, padding: 0, color: "#b8becd", fontSize: "0.88rem", cursor: "pointer", fontFamily: "inherit" }}
className="consent-reopen"
>
{label}
</button>
Expand Down
50 changes: 28 additions & 22 deletions apps/web/src/components/LegalPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Link from "next/link";
import type { Dict, Locale } from "@/lib/i18n";
import "../app/prototype.css";
import "../app/legal.css";

/** The legal pages, each at /{locale}/{doc}. */
export const LEGAL_DOCS = ["terms", "withdrawal", "privacy", "imprint"] as const;
Expand All @@ -26,31 +28,35 @@ export function LegalPage({
const page = l[doc];

return (
<main className="wrap wrap-narrow legal-doc" style={{ padding: "40px 24px 72px" }}>
<h1>{page.title}</h1>
<p className="lede">{page.lede}</p>
{doc !== "imprint" && <p className="note">{l.draft}</p>}
<main className="legal-doc lg">
<div className="pp-band lg-band">
<div className="wrap wrap-narrow">
<h1>{page.title}</h1>
<p className="lg-lede">{page.lede}</p>
</div>
</div>

{page.sections.map((s) => (
<section key={s.h}>
<h2>{s.h}</h2>
{s.p.map((text) => (
<p key={text} style={{ maxWidth: "68ch" }}>
{text}
</p>
<div className="wrap wrap-narrow lg-body">
{doc !== "imprint" && <p className="note lg-draft">{l.draft}</p>}

<div className="lg-card">
{page.sections.map((s) => (
<section key={s.h}>
<h2>{s.h}</h2>
{s.p.map((text) => (
<p key={text}>{text}</p>
))}
</section>
))}
</section>
))}
</div>

<p className="small muted" style={{ marginTop: 36 }}>
{"updated" in page ? page.updated : l.updated}
{LEGAL_DOCS.filter((other) => other !== doc).map((other) => (
<span key={other}>
{" · "}
<Link href={`/${locale}/${other}`}>{l[other].title}</Link>
</span>
))}
</p>
<p className="lg-updated">{"updated" in page ? page.updated : l.updated}</p>
<nav className="lg-more">
{LEGAL_DOCS.filter((other) => other !== doc).map((other) => (
<Link key={other} href={`/${locale}/${other}`}>{l[other].title}</Link>
))}
</nav>
</div>
</main>
);
}
38 changes: 22 additions & 16 deletions apps/web/src/components/SecurityPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Link from "next/link";
import type { Dict, Locale } from "@/lib/i18n";
import { Icon } from "./LineIcon";
import "../app/prototype.css";
import "../app/legal.css";

/**
* Security and privacy at a glance (2026-09-24), the page a business in Austria or Germany reads
Expand All @@ -10,7 +13,7 @@ export function SecurityPage({ locale, d }: { locale: Locale; d: Dict }) {
const s = d.security;

const table = (head: readonly string[], rows: readonly (readonly string[])[]) => (
<div className="tbl-wrap" style={{ marginTop: 12 }}>
<div className="tbl-wrap lg-table">
<table>
<thead>
<tr>{head.map((h) => <th key={h}>{h}</th>)}</tr>
Expand All @@ -25,26 +28,29 @@ export function SecurityPage({ locale, d }: { locale: Locale; d: Dict }) {
);

return (
<main className="legal-doc" style={{ padding: "40px 0 72px" }}>
<div className="wrap">
<h1>{s.title}</h1>
<p className="lede" style={{ maxWidth: "62ch" }}>{s.lede}</p>
<div className="grid" style={{ marginTop: 28 }}>
{s.promises.map((p) => (
<div key={p.h} className="card">
<h3>{p.h}</h3>
<p className="muted" style={{ margin: 0 }}>{p.p}</p>
</div>
))}
<main className="legal-doc lg">
<div className="pp-band lg-band">
<div className="wrap">
<h1>{s.title}</h1>
<p className="lg-lede">{s.lede}</p>
</div>
</div>
<div className="wrap lg-promises">
{s.promises.map((p) => (
<div key={p.h} className="lg-promise">
<span className="lg-promise-ico"><Icon name="shield" /></span>
<h3>{p.h}</h3>
<p>{p.p}</p>
</div>
))}
</div>

<div className="wrap wrap-narrow" style={{ marginTop: 20 }}>
<div className="wrap wrap-narrow lg-body">
{s.sections.map((sec) => (
<section key={sec.h}>
<h2>{sec.h}</h2>
{sec.p.map((text) => (
<p key={text} style={{ maxWidth: "68ch" }}>{text}</p>
<p key={text}>{text}</p>
))}
</section>
))}
Expand All @@ -61,13 +67,13 @@ export function SecurityPage({ locale, d }: { locale: Locale; d: Dict }) {

<section>
<h2>{s.contactTitle}</h2>
<p style={{ maxWidth: "68ch" }}>{s.contact}</p>
<p>{s.contact}</p>
<p>
<Link href={`/${locale}/privacy`}>{s.privacyLink}</Link>
</p>
</section>

<p className="small muted" style={{ marginTop: 36 }}>{s.updated}</p>
<p className="lg-updated">{s.updated}</p>
</div>
</main>
);
Expand Down
Loading