mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(a11y): WCAG AA contrast retune for the Otter Orange palette (#567)
Fixes #557. Vivid fill, ink label: brand #E07832 stays on fills while primary-foreground flips to #1A1814 (5.83:1); new theme-aware ink tokens carry orange, destructive, and success text roles; opacity-modified text purged; landing, demo, and the docs fund button retuned. Guarded by a CSS-parsing unit contrast test, rebuilt axe baselines with zero contrast entries, a new landing axe smoke, and fully regenerated darwin visual baselines.
This commit is contained in:
@@ -62,7 +62,7 @@ const MODALITIES = [
|
||||
label: t(locale, "home.categoryCards.file.label"),
|
||||
blurb: t(locale, "home.categoryCards.file.blurb"),
|
||||
icon: "Database",
|
||||
color: "#5C8642",
|
||||
color: "#51763A",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -77,21 +77,21 @@ const cards = MODALITIES.map((m) => {
|
||||
{cards.map((c) => (
|
||||
<a
|
||||
href={localizeHref(locale, `/tools/${c.section}/`)}
|
||||
class="group flex flex-col rounded-2xl p-4 text-start text-white no-underline shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
||||
style={`background-color: ${c.color}`}
|
||||
class="group flex flex-col rounded-2xl p-4 text-start no-underline shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
||||
style={`background-color: ${c.color}; color: ${c.id === "image" ? "#1A1814" : "#FFFFFF"}`}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20">
|
||||
<span class={`flex h-9 w-9 items-center justify-center rounded-lg ${c.id === "image" ? "bg-white/20" : "bg-black/20"}`}>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={renderIcon(c.icon)} />
|
||||
</span>
|
||||
<span class="rounded-full bg-white/20 px-2.5 py-0.5 text-xs font-semibold">{t(locale, "home.categoryCards.toolCount", { count: c.count })}</span>
|
||||
<span class={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${c.id === "image" ? "bg-white/20" : "bg-black/20"}`}>{t(locale, "home.categoryCards.toolCount", { count: c.count })}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-1.5">
|
||||
<p class="font-display text-base font-bold leading-tight">{c.label}</p>
|
||||
<svg class="h-4 w-4 -translate-x-1 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-white/80">{c.blurb}</p>
|
||||
<p class="mt-1 text-xs">{c.blurb}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,7 @@ const frameworks = ["SOC 2", "ISO 27001", "HIPAA", "GDPR"];
|
||||
set:html={feature.icon}
|
||||
/>
|
||||
</div>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.15em] text-primary/60">{feature.tag}</span>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.15em] text-primary-light">{feature.tag}</span>
|
||||
</div>
|
||||
<h3 class="mt-4 font-display text-base font-bold text-dark-fg">{feature.title}</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-dark-muted">{feature.description}</p>
|
||||
|
||||
@@ -41,7 +41,7 @@ const aiPillColors = [
|
||||
|
||||
<section class="px-6 py-20 md:py-28">
|
||||
<div class="reveal mx-auto mb-16 max-w-3xl text-center md:mb-20">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-primary-dark">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-primary-ink">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
{t(locale, "home.features.badge")}
|
||||
</span>
|
||||
@@ -58,7 +58,7 @@ const aiPillColors = [
|
||||
<!-- Feature 1: Data Sovereignty — text left, visual right -->
|
||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||
<div class="md:order-1">
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f1.kicker")}</span>
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f1.kicker")}</span>
|
||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||
{t(locale, "home.features.f1.title")}
|
||||
</h3>
|
||||
@@ -70,7 +70,7 @@ const aiPillColors = [
|
||||
<div class="rounded-xl border border-border bg-surface p-6 md:p-8">
|
||||
<!-- Outer infrastructure box -->
|
||||
<div class="rounded-lg border-2 border-dashed border-primary/30 p-5">
|
||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary">{t(locale, "home.features.f1.yourInfra")}</span>
|
||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary-ink">{t(locale, "home.features.f1.yourInfra")}</span>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col items-center gap-2 rounded-lg bg-primary-subtle p-4">
|
||||
<svg class="h-8 w-8 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -78,14 +78,14 @@ const aiPillColors = [
|
||||
<path d="M8 12h8M8 16h5" />
|
||||
<circle cx="18" cy="7" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
<span class="text-xs font-semibold text-primary">{t(locale, "home.features.f1.snapotter")}</span>
|
||||
<span class="text-xs font-semibold text-primary-ink">{t(locale, "home.features.f1.snapotter")}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 rounded-lg bg-primary-subtle p-4">
|
||||
<svg class="h-8 w-8 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
||||
<polyline points="13 2 13 9 20 9" />
|
||||
</svg>
|
||||
<span class="text-xs font-semibold text-primary">{t(locale, "home.features.f1.yourFiles")}</span>
|
||||
<span class="text-xs font-semibold text-primary-ink">{t(locale, "home.features.f1.yourFiles")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Internal arrows -->
|
||||
@@ -113,7 +113,7 @@ const aiPillColors = [
|
||||
<!-- Feature 2: REST API — visual left, text right -->
|
||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||
<div class="md:order-2">
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f2.kicker")}</span>
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f2.kicker")}</span>
|
||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||
{t(locale, "home.features.f2.title")}
|
||||
</h3>
|
||||
@@ -138,7 +138,7 @@ const aiPillColors = [
|
||||
<!-- Feature 3: AI Models — text left, visual right -->
|
||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||
<div class="md:order-1">
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f3.kicker")}</span>
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f3.kicker")}</span>
|
||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||
{t(locale, "home.features.f3.title")}
|
||||
</h3>
|
||||
@@ -160,7 +160,7 @@ const aiPillColors = [
|
||||
<!-- Feature 4: Deployment — visual left, text right -->
|
||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||
<div class="md:order-2">
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f4.kicker")}</span>
|
||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f4.kicker")}</span>
|
||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||
{t(locale, "home.features.f4.title")}
|
||||
</h3>
|
||||
|
||||
@@ -52,7 +52,7 @@ const trustBadges = [
|
||||
</h1>
|
||||
|
||||
<!-- Subtitle: capability breadth plus the infrastructure surfaces -->
|
||||
<p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-white/90 md:text-xl" style="animation-delay: 0.15s; text-shadow: 0 1px 16px rgba(90, 20, 0, 0.22);">
|
||||
<p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-white md:text-xl" style="animation-delay: 0.15s; text-shadow: 0 1px 16px rgba(90, 20, 0, 0.22);">
|
||||
{t(locale, "home.hero.subtitle")}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const DOCKER_CMD =
|
||||
---
|
||||
|
||||
<div class="animate-fade-up mt-6 text-center" style="animation-delay: 0.24s;">
|
||||
<p class="mb-3 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">
|
||||
<p class="mb-3 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">
|
||||
{t(locale, "home.quickstart.label")}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ const { locale = "en" } = Astro.props;
|
||||
// Tool name/description come from shared i18n (translated in all 21 langs).
|
||||
const toolStrings = await loadToolStrings(locale);
|
||||
|
||||
const MODALITY_META: Record<string, { label: string; color: string }> = {
|
||||
image: { label: "Image", color: "#E07832" },
|
||||
video: { label: "Video", color: "#BE4A3C" },
|
||||
audio: { label: "Audio", color: "#2C7A75" },
|
||||
document: { label: "PDF", color: "#44568C" },
|
||||
file: { label: "Files", color: "#5C8642" },
|
||||
const MODALITY_META: Record<string, { label: string; color: string; ink: string }> = {
|
||||
image: { label: "Image", color: "#E07832", ink: "#A85518" },
|
||||
video: { label: "Video", color: "#BE4A3C", ink: "#B54639" },
|
||||
audio: { label: "Audio", color: "#2C7A75", ink: "#2B7672" },
|
||||
document: { label: "PDF", color: "#44568C", ink: "#44568C" },
|
||||
file: { label: "Files", color: "#5C8642", ink: "#51763A" },
|
||||
};
|
||||
|
||||
function renderIcon(iconName: string): string {
|
||||
@@ -89,7 +89,7 @@ function renderIcon(iconName: string): string {
|
||||
<span class="flex-1 text-sm font-medium text-foreground">{toolName}</span>
|
||||
<span
|
||||
class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold"
|
||||
style={`background-color: ${mod?.color}1a; color: ${mod?.color}`}
|
||||
style={`background-color: ${mod?.color}1a; color: ${mod?.ink}`}
|
||||
>
|
||||
{mod?.label}
|
||||
</span>
|
||||
|
||||
@@ -19,7 +19,7 @@ const CONTRIBUTE_URL = "https://docs.snapotter.com/guide/translations";
|
||||
hidden
|
||||
>
|
||||
<span>{t(locale, "banner.machineTranslated")} </span>
|
||||
<a href={CONTRIBUTE_URL} target="_blank" rel="noopener noreferrer" class="font-semibold text-primary-dark underline">
|
||||
<a href={CONTRIBUTE_URL} target="_blank" rel="noopener noreferrer" class="font-semibold text-primary-ink underline">
|
||||
{t(locale, "banner.machineTranslated")}
|
||||
</a>
|
||||
<button
|
||||
|
||||
@@ -56,7 +56,7 @@ const checkIcon =
|
||||
<div class="pointer-events-none absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-primary/[0.04] to-transparent"></div>
|
||||
|
||||
<div class="relative">
|
||||
<span class="mb-4 inline-block rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary">
|
||||
<span class="mb-4 inline-block rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary-ink">
|
||||
{t(locale, "home.pricing.oss.badge")}
|
||||
</span>
|
||||
<h3 class="font-display text-2xl font-bold">{t(locale, "home.pricing.oss.name")}</h3>
|
||||
|
||||
@@ -25,7 +25,7 @@ const blocks = [
|
||||
<div class="not-prose space-y-5">
|
||||
{blocks.map((b) => (
|
||||
<div>
|
||||
<p class="mb-2 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">
|
||||
<p class="mb-2 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">
|
||||
{b.label}
|
||||
</p>
|
||||
<button
|
||||
|
||||
@@ -254,12 +254,12 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
class:list={[
|
||||
"tool-command-modality rounded-md px-3 py-2 text-sm font-semibold transition-colors",
|
||||
modality.id === "all"
|
||||
? "bg-primary text-white shadow-sm"
|
||||
? "bg-primary text-foreground shadow-sm"
|
||||
: "text-muted hover:bg-background-alt hover:text-foreground",
|
||||
]}
|
||||
>
|
||||
<span>{modality.label}</span>
|
||||
<span class="ms-1 text-xs opacity-75">{modality.count}</span>
|
||||
<span class="ms-1 text-xs">{modality.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -273,7 +273,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
<button
|
||||
id="tool-command-reset-workflow"
|
||||
type="button"
|
||||
class="hidden rounded-md px-2 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary-subtle"
|
||||
class="hidden rounded-md px-2 py-1 text-xs font-semibold text-primary-ink transition-colors hover:bg-primary-subtle"
|
||||
>
|
||||
{t(locale, "home.toolGrid.reset")}
|
||||
</button>
|
||||
@@ -304,7 +304,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-semibold text-foreground">{workflow.label}</span>
|
||||
<span class="mt-0.5 block text-xs font-medium text-primary-dark">{workflow.metric}</span>
|
||||
<span class="mt-0.5 block text-xs font-medium text-primary-ink">{workflow.metric}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="mt-2 block text-xs leading-relaxed text-muted">{workflow.description}</span>
|
||||
@@ -411,13 +411,13 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
id="tool-command-request"
|
||||
class="font-semibold text-primary hover:underline"
|
||||
class="font-semibold text-primary-ink hover:underline"
|
||||
>
|
||||
{t(locale, "home.toolGrid.requestTool")}
|
||||
</a>
|
||||
{t(locale, "home.toolGrid.requestSuffix")}
|
||||
</div>
|
||||
<a href={localizeHref(locale, "/tools")} class="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:underline">
|
||||
<a href={localizeHref(locale, "/tools")} class="inline-flex items-center gap-2 text-sm font-semibold text-primary-ink hover:underline">
|
||||
{t(locale, "home.toolGrid.browseCatalog")}
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -545,7 +545,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
const reason = formatReason(result.reason);
|
||||
const reasonHtml =
|
||||
showReason && reason
|
||||
? `<span class="mt-3 inline-flex w-fit rounded-md bg-primary-subtle px-2 py-1 text-[11px] font-semibold text-primary-dark">${escapeHtml(reason)}</span>`
|
||||
? `<span class="mt-3 inline-flex w-fit rounded-md bg-primary-subtle px-2 py-1 text-[11px] font-semibold text-primary-ink">${escapeHtml(reason)}</span>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
@@ -580,7 +580,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
const isActive = button.dataset.modality === activeModality;
|
||||
button.setAttribute("aria-pressed", String(isActive));
|
||||
button.classList.toggle("bg-primary", isActive);
|
||||
button.classList.toggle("text-white", isActive);
|
||||
button.classList.toggle("text-foreground", isActive);
|
||||
button.classList.toggle("shadow-sm", isActive);
|
||||
button.classList.toggle("text-muted", !isActive);
|
||||
button.classList.toggle("hover:bg-background-alt", !isActive);
|
||||
|
||||
@@ -43,7 +43,7 @@ const stats = [
|
||||
{stats.map((stat) => {
|
||||
const inner = (
|
||||
<div class="text-center">
|
||||
<p class="font-display text-5xl font-extrabold tracking-tight text-primary md:text-6xl">
|
||||
<p class="font-display text-5xl font-extrabold tracking-tight text-primary-ink md:text-6xl">
|
||||
{stat.value}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-semibold text-foreground">{stat.label}</p>
|
||||
|
||||
@@ -19,7 +19,7 @@ const locale = "en";
|
||||
|
||||
<main id="main" class="flex min-h-[60vh] flex-col items-center justify-center px-6 pt-32 pb-20 text-center">
|
||||
<div class="reveal">
|
||||
<p class="text-6xl font-bold text-primary">{t(locale, "notFound.code")}</p>
|
||||
<p class="text-6xl font-bold text-primary-ink">{t(locale, "notFound.code")}</p>
|
||||
<h1 class="mt-4 font-display text-3xl font-bold tracking-tight md:text-4xl">
|
||||
{t(locale, "notFound.title")}
|
||||
</h1>
|
||||
|
||||
@@ -115,7 +115,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
||||
<!-- Hero -->
|
||||
<section class="mx-auto max-w-4xl px-6 pt-8 pb-12">
|
||||
<div class="reveal">
|
||||
<span class="rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-dark">
|
||||
<span class="rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-ink">
|
||||
{alt.category}
|
||||
</span>
|
||||
<h1 class="mt-5 font-display text-4xl font-bold tracking-tight md:text-5xl">
|
||||
@@ -160,7 +160,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
||||
<thead class="border-b border-border bg-background-alt">
|
||||
<tr>
|
||||
<th class="px-5 py-4 text-start font-semibold"> </th>
|
||||
<th class="px-5 py-4 text-start font-semibold text-primary-dark">SnapOtter</th>
|
||||
<th class="px-5 py-4 text-start font-semibold text-primary-ink">SnapOtter</th>
|
||||
<th class="px-5 py-4 text-start font-semibold">{alt.competitor}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -196,7 +196,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary transition-colors hover:text-primary-dark"
|
||||
class="text-primary-ink transition-colors hover:text-primary-ink-strong"
|
||||
>
|
||||
{source.label}
|
||||
</a>
|
||||
|
||||
@@ -91,7 +91,7 @@ const formatReviewDate = (date: string) =>
|
||||
href={localizeHref(locale, `/alternatives/${alt.slug}`)}
|
||||
class="group flex flex-col rounded-xl border border-border bg-surface p-6 transition-all hover:border-primary hover:shadow-md"
|
||||
>
|
||||
<span class="text-xs font-semibold text-primary-dark">{alt.category}</span>
|
||||
<span class="text-xs font-semibold text-primary-ink">{alt.category}</span>
|
||||
<span class="mt-2 font-display text-lg font-bold tracking-tight">
|
||||
{alt.competitorOpenSource
|
||||
? t(locale, "altIndex.card.versus", { competitor: alt.competitor })
|
||||
@@ -103,7 +103,7 @@ const formatReviewDate = (date: string) =>
|
||||
<span class="mt-4 text-xs text-muted">
|
||||
{t(locale, "altIndex.card.reviewed", { date: formatReviewDate(alt.lastReviewed) })}
|
||||
</span>
|
||||
<span class="mt-auto inline-flex items-center gap-1 pt-5 text-sm font-medium text-primary transition-all group-hover:gap-2">
|
||||
<span class="mt-auto inline-flex items-center gap-1 pt-5 text-sm font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||
{t(locale, "altIndex.card.compare")}
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||
|
||||
@@ -90,7 +90,7 @@ const subjects = [
|
||||
</div>
|
||||
|
||||
<p class="mt-10 text-sm text-muted">
|
||||
{t(locale, "contact.emailPrefix")} <a href="mailto:contact@snapotter.com" class="font-medium text-primary hover:underline">contact@snapotter.com</a>
|
||||
{t(locale, "contact.emailPrefix")} <a href="mailto:contact@snapotter.com" class="font-medium text-primary-ink hover:underline">contact@snapotter.com</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -96,10 +96,10 @@ const permissionMatrix = {
|
||||
};
|
||||
|
||||
const storageProviders = [
|
||||
{ name: t(locale, "enterprise.storage.provider.awsS3"), color: "text-[#FF9900]" },
|
||||
{ name: t(locale, "enterprise.storage.provider.awsS3"), color: "text-[#996306]" },
|
||||
{ name: t(locale, "enterprise.storage.provider.minio"), color: "text-[#C72C48]" },
|
||||
{ name: t(locale, "enterprise.storage.provider.r2"), color: "text-[#F6821F]" },
|
||||
{ name: t(locale, "enterprise.storage.provider.local"), color: "text-primary" },
|
||||
{ name: t(locale, "enterprise.storage.provider.r2"), color: "text-[#B45309]" },
|
||||
{ name: t(locale, "enterprise.storage.provider.local"), color: "text-primary-ink" },
|
||||
];
|
||||
|
||||
const deploymentTargets = [
|
||||
@@ -273,10 +273,10 @@ const xSvg =
|
||||
</div>
|
||||
<div class="md:order-2">
|
||||
<div class="rounded-xl border border-border bg-surface p-6">
|
||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary">{t(locale, "enterprise.identity.supportedProviders")}</span>
|
||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary-ink">{t(locale, "enterprise.identity.supportedProviders")}</span>
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
{identityProviders.map((provider) => (
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary-subtle px-3.5 py-1.5 text-sm font-medium text-primary-dark">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary-subtle px-3.5 py-1.5 text-sm font-medium text-primary-ink">
|
||||
<svg class="h-3.5 w-3.5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{provider}
|
||||
</span>
|
||||
@@ -286,7 +286,7 @@ const xSvg =
|
||||
<svg class="h-4 w-4 shrink-0 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
<span class="text-xs text-primary-dark">{t(locale, "enterprise.identity.mfaEnforced")}</span>
|
||||
<span class="text-xs text-primary-ink">{t(locale, "enterprise.identity.mfaEnforced")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +304,7 @@ const xSvg =
|
||||
</div>
|
||||
<div class="md:order-1">
|
||||
<div class="rounded-xl border border-border bg-surface p-6 overflow-x-auto">
|
||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary">{t(locale, "enterprise.permissions.matrix")}</span>
|
||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary-ink">{t(locale, "enterprise.permissions.matrix")}</span>
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -407,7 +407,7 @@ const xSvg =
|
||||
<th class="pb-4 text-start text-sm font-semibold text-muted"></th>
|
||||
<th class="pb-4 text-start text-sm font-semibold text-muted">{t(locale, "enterprise.comparison.cloudHeader")}</th>
|
||||
<th class="pb-4 text-start text-sm font-semibold">
|
||||
<span class="text-primary">SnapOtter</span> <span class="text-muted font-normal">{t(locale, "enterprise.comparison.selfHostedHeader")}</span>
|
||||
<span class="text-primary-ink">SnapOtter</span> <span class="text-muted font-normal">{t(locale, "enterprise.comparison.selfHostedHeader")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -131,7 +131,7 @@ const breadcrumbJsonLd = {
|
||||
{t(locale, "privacy.contact.pre")}
|
||||
<a
|
||||
href="mailto:contact@snapotter.com"
|
||||
class="text-primary font-medium hover:underline underline-offset-4"
|
||||
class="text-primary-ink font-medium hover:underline underline-offset-4"
|
||||
>
|
||||
contact@snapotter.com
|
||||
</a>.
|
||||
|
||||
@@ -80,7 +80,7 @@ const breadcrumbJsonLd = {
|
||||
{t(locale, "terms.license.item4.pre")}
|
||||
<a
|
||||
href="mailto:contact@snapotter.com"
|
||||
class="text-primary font-medium hover:underline underline-offset-4"
|
||||
class="text-primary-ink font-medium hover:underline underline-offset-4"
|
||||
>
|
||||
contact@snapotter.com
|
||||
</a>
|
||||
@@ -134,7 +134,7 @@ const breadcrumbJsonLd = {
|
||||
{t(locale, "terms.contact.pre")}
|
||||
<a
|
||||
href="mailto:contact@snapotter.com"
|
||||
class="text-primary font-medium hover:underline underline-offset-4"
|
||||
class="text-primary-ink font-medium hover:underline underline-offset-4"
|
||||
>
|
||||
contact@snapotter.com
|
||||
</a>.
|
||||
|
||||
@@ -104,7 +104,7 @@ const toolDesc = (id: string, fallback: string) => toolStrings[id]?.description
|
||||
<div class="min-w-0">
|
||||
<span class="text-sm font-bold">{toolName(tool.id, tool.name)}</span>
|
||||
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">{toolDesc(tool.id, tool.description)}</p>
|
||||
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
||||
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||
{t(locale, "common.learnMore")}
|
||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||
|
||||
@@ -173,7 +173,7 @@ const collectionJsonLd = {
|
||||
data-modality={m.id}
|
||||
class:list={[
|
||||
"tools-mod-btn shrink-0 rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
|
||||
m.id === "all" ? "bg-primary text-white" : "border border-border bg-surface hover:bg-background-alt",
|
||||
m.id === "all" ? "bg-primary text-foreground" : "border border-border bg-surface hover:bg-background-alt",
|
||||
]}
|
||||
>
|
||||
{m.label} ({m.count})
|
||||
@@ -263,7 +263,7 @@ const collectionJsonLd = {
|
||||
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">
|
||||
{toolDesc(tool.id, tool.description)}
|
||||
</p>
|
||||
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
||||
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||
{t(locale, "common.learnMore")}
|
||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||
@@ -341,7 +341,7 @@ const collectionJsonLd = {
|
||||
const on = b.getAttribute("data-modality") === activeMod;
|
||||
b.className =
|
||||
"tools-mod-btn shrink-0 rounded-full px-4 py-1.5 text-sm font-medium transition-colors " +
|
||||
(on ? "bg-primary text-white" : "border border-border bg-surface hover:bg-background-alt");
|
||||
(on ? "bg-primary text-foreground" : "border border-border bg-surface hover:bg-background-alt");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
||||
<h1 class="font-display text-4xl font-bold tracking-tight md:text-5xl">{uc.h1}</h1>
|
||||
<p class="mt-4 text-lg leading-relaxed text-muted md:text-xl">{uc.intro}</p>
|
||||
{uc.isAiBundle && (
|
||||
<p class="mt-4 inline-flex rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-dark">
|
||||
<p class="mt-4 inline-flex rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-ink">
|
||||
Local AI, one-time bundle install
|
||||
</p>
|
||||
)}
|
||||
@@ -119,12 +119,12 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
||||
</div>
|
||||
<div class="mt-6 flex flex-wrap gap-x-6 gap-y-2 text-sm">
|
||||
{uc.toolRoutes.map((route, i) => (
|
||||
<a href={route} class="text-primary transition-colors hover:text-primary-dark">
|
||||
<a href={route} class="text-primary-ink transition-colors hover:text-primary-ink-strong">
|
||||
Open the {uc.toolIds[i]} tool
|
||||
</a>
|
||||
))}
|
||||
{alt && (
|
||||
<a href={`/alternatives/${alt.slug}`} class="text-primary transition-colors hover:text-primary-dark">
|
||||
<a href={`/alternatives/${alt.slug}`} class="text-primary-ink transition-colors hover:text-primary-ink-strong">
|
||||
Compare with {alt.competitor}
|
||||
</a>
|
||||
)}
|
||||
@@ -135,7 +135,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
||||
{uc.sources.map((s, i) => (
|
||||
<Fragment>
|
||||
{i > 0 && ", "}
|
||||
<a href={s.url} target="_blank" rel="noopener noreferrer" class="text-primary hover:text-primary-dark">{s.label}</a>
|
||||
<a href={s.url} target="_blank" rel="noopener noreferrer" class="text-primary-ink hover:text-primary-ink-strong">{s.label}</a>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
|
||||
@@ -32,7 +32,7 @@ const description =
|
||||
<a href={`/self-hosted/${uc.slug}`} class="group flex flex-col rounded-xl border border-border bg-surface p-6 transition-all hover:border-primary hover:shadow-md">
|
||||
<span class="font-display text-lg font-bold">{uc.h1}</span>
|
||||
<span class="mt-2 text-sm text-muted">{uc.intro}</span>
|
||||
<span class="mt-4 text-xs font-semibold text-primary">Read more →</span>
|
||||
<span class="mt-4 text-xs font-semibold text-primary-ink">Read more →</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -340,7 +340,7 @@ const allJsonLd = [breadcrumbJsonLd, softwareJsonLd];
|
||||
/>
|
||||
<span class="mt-3 text-sm font-bold">{t.name}</span>
|
||||
<p class="mt-1.5 text-xs leading-relaxed text-muted">{t.description}</p>
|
||||
<span class="mt-auto inline-flex items-center gap-1 pt-4 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
||||
<span class="mt-auto inline-flex items-center gap-1 pt-4 text-xs font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||
Learn more
|
||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||
|
||||
@@ -35,6 +35,8 @@ html {
|
||||
--color-primary-light: #F09550;
|
||||
--color-primary-dark: #C06520;
|
||||
--color-primary-subtle: #FFF5ED;
|
||||
--color-primary-ink: #A85518;
|
||||
--color-primary-ink-strong: #8F4712;
|
||||
|
||||
/* Neutrals -- Warm Cream & Stone (sandy riverbank) */
|
||||
--color-background: #FAFAF7;
|
||||
@@ -59,8 +61,8 @@ html {
|
||||
--color-terminal-green: #4ADE80;
|
||||
|
||||
/* Semantic */
|
||||
--color-success: #2D9D78;
|
||||
--color-danger: #D44C47;
|
||||
--color-success: #1E7B5C;
|
||||
--color-danger: #BE3A35;
|
||||
|
||||
/* Fonts */
|
||||
--font-display: "Bricolage Grotesque", system-ui, sans-serif;
|
||||
@@ -281,7 +283,7 @@ code, pre, kbd {
|
||||
/* ─── BUTTONS ─── */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
|
||||
color: white;
|
||||
color: var(--color-foreground);
|
||||
transition: box-shadow 0.2s, transform 0.1s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
@@ -318,7 +320,7 @@ code, pre, kbd {
|
||||
.code-block .code-string { color: #C3E88D; }
|
||||
.code-block .code-flag { color: #82AAFF; }
|
||||
.code-block .code-url { color: #89DDFF; }
|
||||
.code-block .code-comment { color: #546E7A; }
|
||||
.code-block .code-comment { color: #7E99A8; }
|
||||
|
||||
/* ─── SECTION DIVIDER ─── */
|
||||
.section-divider {
|
||||
@@ -343,7 +345,7 @@ code, pre, kbd {
|
||||
z-index: 100;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
color: var(--color-foreground);
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
transition: top 0.2s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user