diff --git a/components/landing/HighlightedCode.tsx b/components/landing/HighlightedCode.tsx index 5ddd8d3d..d6d4348c 100644 --- a/components/landing/HighlightedCode.tsx +++ b/components/landing/HighlightedCode.tsx @@ -10,7 +10,7 @@ function getHighlighter(): Promise { highlighterPromise = import("shiki").then((shiki) => shiki.createHighlighter({ themes: ["github-dark"], - langs: ["bash", "json", "typescript"], + langs: ["bash", "json", "typescript", "html", "dart"], }) ); } @@ -19,7 +19,7 @@ function getHighlighter(): Promise { interface HighlightedCodeProps { code: string; - lang?: "bash" | "json" | "typescript"; + lang?: "bash" | "json" | "typescript" | "html" | "dart"; className?: string; } diff --git a/components/landing/mix-tailwinds/ComparisonSeam.tsx b/components/landing/mix-tailwinds/ComparisonSeam.tsx new file mode 100644 index 00000000..6a39e588 --- /dev/null +++ b/components/landing/mix-tailwinds/ComparisonSeam.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useId, useState } from "react"; +import type { CSSProperties } from "react"; +import { NARROW_CAPTURE_QUERY, type Example } from "./content"; + +/* Both halves are real captures of the same class string: a browser rendering + Tailwind on the left, Flutter rendering mix_tailwinds on the right. Narrow + screens get the phone-width capture, because the desktop one shrinks past + legibility and the comparison stops meaning anything. + + Built on a native range input so pointer, touch, and keyboard all work + without reimplementing any of them, and screen readers announce a slider. */ +export function ComparisonSeam({ example }: { example: Example }) { + const [position, setPosition] = useState(52); + const labelId = useId(); + + const frameStyle = { + "--mtw-ratio": example.ratio, + "--mtw-ratio-sm": example.ratioSm, + } as CSSProperties; + + return ( +
+ + + {`${example.title} + + + + + {`${example.title} + + + + + + + + + setPosition(Number(event.target.value))} + /> +
+ ); +} diff --git a/components/landing/mix-tailwinds/MixTailwindsLanding.tsx b/components/landing/mix-tailwinds/MixTailwindsLanding.tsx new file mode 100644 index 00000000..4958279f --- /dev/null +++ b/components/landing/mix-tailwinds/MixTailwindsLanding.tsx @@ -0,0 +1,260 @@ +"use client"; + +import { useRef, useState } from "react"; +import type { KeyboardEvent } from "react"; +import { motion, MotionConfig } from "framer-motion"; +import { HighlightedCode } from "../HighlightedCode"; +import { LandingButton } from "../LandingButton"; +import { fadeUp, reveal, staggerChild, staggerParent } from "../motion"; +import { SectionHead } from "../SectionHead"; +import { Aurora } from "../sections/Aurora"; +import { ComparisonSeam } from "./ComparisonSeam"; +import { + DART_SNIPPET, + EXAMPLES, + FEATURES, + HTML_SNIPPET, + INSTALL_SNIPPET, +} from "./content"; +import "../landing.css"; +import "./mix-tailwinds.css"; + +const PUB_DEV_URL = "https://pub.dev/packages/mix_tailwinds"; +const GITHUB_URL = + "https://github.com/btwld/mix/tree/main/packages/mix_tailwinds"; + +const exampleTabId = (slug: string) => `mtw-example-tab-${slug}`; +const examplePanelId = (slug: string) => `mtw-example-panel-${slug}`; + +function CodePane({ + language, + lang, + filename, + code, +}: { + language: string; + lang: "html" | "dart"; + filename: string; + code: string; +}) { + return ( +
+
+ {language} + {filename} +
+ +
+ ); +} + +export function MixTailwindsLanding() { + const [activeSlug, setActiveSlug] = useState(EXAMPLES[0].slug); + const tabRefs = useRef>([]); + const active = EXAMPLES.find((e) => e.slug === activeSlug) ?? EXAMPLES[0]; + + const handleExampleKeyDown = ( + event: KeyboardEvent, + index: number, + ) => { + let nextIndex: number; + + switch (event.key) { + case "ArrowRight": + nextIndex = (index + 1) % EXAMPLES.length; + break; + case "ArrowLeft": + nextIndex = (index - 1 + EXAMPLES.length) % EXAMPLES.length; + break; + case "Home": + nextIndex = 0; + break; + case "End": + nextIndex = EXAMPLES.length - 1; + break; + default: + return; + } + + event.preventDefault(); + const nextExample = EXAMPLES[nextIndex]; + setActiveSlug(nextExample.slug); + tabRefs.current[nextIndex]?.focus(); + }; + + return ( + +
+ + +
+ + + mix_tailwinds + Alpha + + + + Tailwind classes. +
+ Native Flutter. +
+ + + Write the utilities you already know and get real Flutter widgets + — not a webview, not a screenshot. + + + + + Get started + + + View on GitHub + + + + + + +
+
+ +
+ + Real components, no CSS + + } + lead="Every example uses the same class string in Tailwind and Flutter. Pick one, then drag the seam to compare both renderings." + /> + + +
+ {EXAMPLES.map((example, index) => ( + + ))} +
+ + {EXAMPLES.map((example) => ( + + ))} +
+
+ +
+ + + + + + + +
+ +
+ + + + {FEATURES.map((feature) => ( + +

{feature.title}

+

{feature.body}

+
+ ))} +
+
+ +
+ +

Start with one class string.

+

+ mix_tailwinds is an open-source alpha built on Mix. Add it to a + Flutter project and style your first widget the way you already + think about layout. +

+
+ + Install from pub.dev + + + Read the source + +
+
+
+
+
+ ); +} diff --git a/components/landing/mix-tailwinds/content.ts b/components/landing/mix-tailwinds/content.ts new file mode 100644 index 00000000..077880c2 --- /dev/null +++ b/components/landing/mix-tailwinds/content.ts @@ -0,0 +1,112 @@ +/* Content for the mix_tailwinds landing page. + + The captures under public/mix-tailwinds/ are produced by the package's own + comparison suite: each example is rendered once in a real browser on + Tailwind 4.3.1 and once in Flutter, from the same class string. Regenerate + them with `npm run compare:advanced` in packages/mix_tailwinds/tool/ + visual-comparison and copy the per-example captures across. The suite keeps + a 16px diagnostic margin around every capture; the landing crops that margin + at presentation time while preserving the source evidence. */ + +export type Example = { + slug: string; + title: string; + /** Ratio of the visible desktop capture after cropping its 16px margin. */ + ratio: string; + /** Ratio of the visible phone capture after cropping its 16px margin. */ + ratioSm: string; +}; + +/* Below this the desktop capture is too small to read, so the narrow capture — + the same component laid out for a phone — is served instead. Matches the CSS + breakpoint in mix-tailwinds.css. */ +export const NARROW_CAPTURE_QUERY = "(max-width: 720px)"; + +export const EXAMPLES: Example[] = [ + { + slug: "launch-command", + title: "Launch command", + ratio: "976 / 476", + ratioSm: "432 / 786", + }, + { + slug: "signal-analytics", + title: "Signal analytics", + ratio: "976 / 570", + ratioSm: "432 / 1007", + }, + { + slug: "incident-room", + title: "Incident room", + ratio: "976 / 627", + ratioSm: "432 / 995", + }, + { + slug: "release-timeline", + title: "Release timeline", + ratio: "976 / 488", + ratioSm: "432 / 1116", + }, + { + slug: "capacity-map", + title: "Capacity map", + ratio: "976 / 595", + ratioSm: "432 / 988", + }, +]; + +/* Every class string stays on one line in both panes. Splitting a Dart string + across lines needs adjacent-literal concatenation, and that plumbing reads as + noise next to the markup it is supposed to mirror — so the panes are sized to + fit these instead of the strings being broken to fit the panes. */ +export const HTML_SNIPPET = `
+

Northstar

+

Atlas

+ + +
`; + +/* The functional API: lowercase helpers that read like the markup above. */ +export const DART_SNIPPET = `div('flex flex-col gap-4 rounded-3xl bg-slate-950 p-6', [ + p('text-sm text-slate-400', 'Northstar'), + h2('text-3xl font-bold text-white md:text-4xl', 'Atlas'), + + button( + 'rounded-full bg-indigo-500 px-5 py-2 hover:bg-indigo-400', + [span('text-sm font-semibold text-white', 'Deploy')], + onPressed: deploy, + ), +])`; + +export const INSTALL_SNIPPET = `flutter pub add mix_tailwinds`; + +export const FEATURES = [ + { + title: "Real Flutter widgets", + body: "Classes compile to Mix stylers and render as native widgets. No webview, no HTML bridge, no runtime CSS engine.", + }, + { + title: "The utilities you know", + body: "Tailwind v4 grammar: responsive prefixes, state variants, arbitrary values, gradients, and the full spacing and color scales.", + }, + { + title: "Interaction states included", + body: "hover:, focus-visible:, active:, and disabled: map onto Flutter widget states, with keyboard and semantics wired up.", + }, + { + title: "Tailwind's theme, generated", + body: "The default palette, radii, shadows, and type scale come from a pinned upstream Tailwind snapshot, not hand-copied values.", + }, + { + title: "Nothing fails silently", + body: "A token that can't be honored is reported through onDiagnostic with a reason and a workaround, never dropped without a word.", + }, + { + title: "Drops down to Mix", + body: "Reach past the class string into typed Mix stylers whenever you need something the utility grammar doesn't express.", + }, +] as const; diff --git a/components/landing/mix-tailwinds/mix-tailwinds.css b/components/landing/mix-tailwinds/mix-tailwinds.css new file mode 100644 index 00000000..98db1802 --- /dev/null +++ b/components/landing/mix-tailwinds/mix-tailwinds.css @@ -0,0 +1,503 @@ +/* mix_tailwinds landing page. + + Accent: iris, not another cyan. The sibling product pages already hold the + blue-to-cyan range, and this page is about translation between two systems, + so its accent sits between Flutter's blue and web tooling's magenta. Jade is + the secondary and appears only where something is confirmed working. + + Type: this page sets the display face in the mono stack. For a utility-class + product the class string is the logo, the headline, and the demo at once, so + the monospace grid is the brand rather than a code-block detail. Inter + carries prose only. + + Tokens are scoped to .mtw-root rather than html[data-product] because this is + a page inside the Mix site, not a separate product identity in the navbar. */ + +.mtw-root { + --lp-accent: #7c6cff; + --lp-accent-hover: #9a8dff; + --lp-accent-low: rgba(124, 108, 255, 0.1); + --lp-accent-glow: rgba(124, 108, 255, 0.2); + --lp-accent-2: #2dd4a7; + --lp-gradient-mid: #c7c0ff; + --lp-aurora-b-glow: rgba(45, 212, 167, 0.14); + + --mtw-shell: min(1140px, 100% - 2.5rem); + --mtw-mono: var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, monospace; + --mtw-panel: color-mix(in srgb, var(--lp-surface) 78%, transparent); + + overflow-x: clip; +} + +.mtw-sr { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.mtw-accent { + background: linear-gradient(100deg, var(--lp-accent), var(--lp-accent-2)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; +} + +/* ---------- hero ---------- */ + +.mtw-hero { + width: var(--mtw-shell); + margin-inline: auto; + padding-block: clamp(4.5rem, 11vw, 7.5rem) clamp(1.5rem, 3vw, 2.5rem); +} + +.mtw-hero-copy { + max-width: 44rem; +} + +.mtw-eyebrow { + display: inline-flex; + align-items: center; + gap: 0.625rem; + margin: 0; + font-family: var(--mtw-mono); + font-size: 0.8125rem; + letter-spacing: 0.04em; + color: var(--lp-text-muted); +} + +.mtw-eyebrow-badge { + border: 1px solid color-mix(in srgb, var(--lp-accent) 45%, transparent); + border-radius: 999px; + background: var(--lp-accent-low); + padding: 0.125rem 0.5rem; + font-size: 0.6875rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--lp-accent); +} + +.mtw-h1 { + margin: 1.25rem 0 0; + font-family: var(--mtw-mono); + font-size: clamp(2.35rem, 5.9vw, 4.15rem); + font-weight: 700; + line-height: 1.02; + letter-spacing: -0.045em; + color: var(--lp-text-main); +} + +.mtw-h1 em { + font-style: normal; + background: linear-gradient( + 100deg, + var(--lp-accent) 0%, + var(--lp-gradient-mid) 46%, + var(--lp-accent-2) 100% + ); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; +} + +.mtw-lead { + margin: 1.5rem 0 0; + max-width: 38rem; + font-size: 1.0625rem; + line-height: 1.7; + color: var(--lp-text-muted); +} + +.mtw-cta-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 2rem; +} + +.mtw-install { + margin-top: 1.5rem; + max-width: 26rem; +} + +/* HighlightedCode zeroes padding and background on its inner
 with
+   important utilities, so the frame has to live on the wrapper it labels. */
+.mtw-install-code {
+  border: 1px solid var(--lp-border-card);
+  border-left: 2px solid var(--lp-accent);
+  border-radius: 0.625rem;
+  background: var(--mtw-panel);
+  padding: 0.75rem 1rem;
+  overflow-x: auto;
+  font-family: var(--mtw-mono);
+}
+
+/* ---------- seam ---------- */
+
+/* The comparison suite leaves 16px around each capture. Enlarge the source
+   from its visible 976px width to 1008px so the frame crops that margin. */
+.mtw-seam {
+  --mtw-capture-width: 103.278689%;
+
+  position: relative;
+  margin: 0;
+  aspect-ratio: var(--mtw-ratio);
+  border: 1px solid var(--lp-border-card);
+  /* Match the scaled rounded-3xl capture corner so its white canvas cannot
+     show between the component and the frame's clipping edge. */
+  border-radius: clamp(1.25rem, 2.5vw, 1.75rem);
+  overflow: hidden;
+  background: var(--lp-surface);
+  box-shadow: 0 32px 80px -32px var(--lp-accent-glow);
+  isolation: isolate;
+}
+
+.mtw-seam-clip {
+  position: absolute;
+  inset: 0;
+  z-index: 2;
+}
+
+.mtw-seam-layer {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: var(--mtw-capture-width);
+  max-width: none;
+  height: auto;
+  translate: -50% -50%;
+  user-select: none;
+  -webkit-user-drag: none;
+}
+
+.mtw-seam-tag {
+  position: absolute;
+  top: 0.75rem;
+  z-index: 3;
+  border-radius: 999px;
+  background: rgba(5, 4, 10, 0.74);
+  backdrop-filter: blur(6px);
+  padding: 0.25rem 0.625rem;
+  font-family: var(--mtw-mono);
+  font-size: 0.6875rem;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  color: #e9e7f5;
+  pointer-events: none;
+}
+
+.mtw-seam-tag-left {
+  left: 0.75rem;
+}
+
+.mtw-seam-tag-right {
+  right: 0.75rem;
+}
+
+.mtw-seam-line {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  z-index: 4;
+  width: 2px;
+  margin-left: -1px;
+  background: linear-gradient(
+    to bottom,
+    transparent,
+    var(--lp-accent) 12%,
+    var(--lp-accent-2) 88%,
+    transparent
+  );
+  box-shadow: 0 0 18px var(--lp-accent-glow);
+  pointer-events: none;
+}
+
+.mtw-seam-grip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  display: grid;
+  place-items: center;
+  translate: -50% -50%;
+  width: 2.25rem;
+  height: 2.25rem;
+  border: 1px solid color-mix(in srgb, var(--lp-accent) 55%, transparent);
+  border-radius: 999px;
+  background: rgba(5, 4, 10, 0.9);
+  backdrop-filter: blur(8px);
+  box-shadow: 0 6px 24px rgba(5, 4, 10, 0.6);
+  color: var(--lp-accent);
+}
+
+.mtw-seam-grip svg {
+  width: 1.125rem;
+  height: 1.125rem;
+}
+
+/* A real range input carries pointer, touch, and keyboard for free. */
+.mtw-seam-input {
+  position: absolute;
+  inset: 0;
+  z-index: 5;
+  width: 100%;
+  height: 100%;
+  margin: 0;
+  opacity: 0;
+  cursor: ew-resize;
+  -webkit-appearance: none;
+  appearance: none;
+  background: transparent;
+}
+
+.mtw-seam-input::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  width: 44px;
+  height: 100%;
+}
+
+.mtw-seam-input::-moz-range-thumb {
+  width: 44px;
+  height: 100%;
+  border: 0;
+  opacity: 0;
+}
+
+.mtw-seam:has(.mtw-seam-input:focus-visible) {
+  outline: 2px solid var(--lp-accent);
+  outline-offset: 3px;
+}
+
+/* ---------- sections ---------- */
+
+.mtw-section {
+  width: var(--mtw-shell);
+  margin-inline: auto;
+  padding-block: clamp(3rem, 6vw, 4.5rem);
+}
+
+/* The code section runs wider than the reading shell so both panes can hold a
+   full class string on one line — the class string is what it is showing. */
+.mtw-section-wide {
+  width: min(1320px, 100% - 2.5rem);
+}
+
+/* ---------- example gallery ---------- */
+
+.mtw-gallery {
+  margin-top: 2.5rem;
+}
+
+.mtw-picker {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.375rem;
+  margin-bottom: 1.25rem;
+}
+
+.mtw-picker-tab {
+  border: 1px solid var(--lp-border-card);
+  border-radius: 999px;
+  background: transparent;
+  padding: 0.4375rem 0.875rem;
+  font-family: var(--mtw-mono);
+  font-size: 0.75rem;
+  color: var(--lp-text-muted);
+  cursor: pointer;
+  transition: color 0.2s, border-color 0.2s, background-color 0.2s;
+}
+
+.mtw-picker-tab:hover {
+  color: var(--lp-text-main);
+  border-color: color-mix(in srgb, var(--lp-accent) 40%, transparent);
+}
+
+.mtw-picker-tab[aria-selected="true"] {
+  background: var(--lp-accent-low);
+  border-color: var(--lp-accent);
+  color: var(--lp-accent);
+}
+
+.mtw-picker-tab:focus-visible {
+  outline: 2px solid var(--lp-accent);
+  outline-offset: 2px;
+}
+
+/* ---------- code panes ---------- */
+
+.mtw-panes {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
+  align-items: center;
+  gap: 1rem;
+  margin-top: 2.5rem;
+}
+
+.mtw-pane {
+  border: 1px solid var(--lp-border-card);
+  border-radius: 0.875rem;
+  background: var(--mtw-panel);
+  overflow: hidden;
+  min-width: 0;
+}
+
+.mtw-pane-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 1rem;
+  border-bottom: 1px solid var(--lp-border-card);
+  padding: 0.625rem 1rem;
+  font-family: var(--mtw-mono);
+  font-size: 0.6875rem;
+}
+
+.mtw-pane-lang {
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: var(--lp-accent);
+}
+
+.mtw-pane-file {
+  color: var(--lp-text-muted);
+}
+
+.mtw-pane-code {
+  padding: 1.125rem 1rem;
+  overflow-x: auto;
+  font-family: var(--mtw-mono);
+}
+
+/* HighlightedCode pins the code size with an important utility inside a
+   cascade layer, which outranks an unlayered override no matter its
+   specificity. The snippets are written to fit 14px instead. */
+
+.mtw-panes-seam {
+  display: grid;
+  place-items: center;
+  font-family: var(--mtw-mono);
+  font-size: 0.6875rem;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: var(--lp-text-muted);
+  white-space: nowrap;
+}
+
+.mtw-panes-seam span {
+  writing-mode: vertical-rl;
+  border-left: 1px solid var(--lp-border-card);
+  border-right: 1px solid var(--lp-border-card);
+  padding: 1.25rem 0.75rem;
+}
+
+/* ---------- features ---------- */
+
+.mtw-features {
+  display: grid;
+  /* 20rem lands on three columns at the shell width, so six cards fill two
+     even rows instead of leaving orphans on the second. */
+  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+  gap: 1rem;
+  margin: 2.5rem 0 0;
+  padding: 0;
+  list-style: none;
+}
+
+.mtw-feature {
+  border: 1px solid var(--lp-border-card);
+  border-radius: 0.875rem;
+  background: var(--mtw-panel);
+  padding: 1.375rem 1.5rem 1.5rem;
+}
+
+.mtw-feature-title {
+  margin: 0;
+  font-family: var(--mtw-mono);
+  font-size: 0.9375rem;
+  font-weight: 600;
+  letter-spacing: -0.01em;
+  color: var(--lp-text-main);
+}
+
+.mtw-feature-body {
+  margin: 0.625rem 0 0;
+  font-size: 0.875rem;
+  line-height: 1.65;
+  color: var(--lp-text-muted);
+}
+
+/* ---------- close ---------- */
+
+.mtw-close {
+  width: var(--mtw-shell);
+  margin-inline: auto;
+  padding-block: clamp(3.5rem, 8vw, 6.5rem);
+}
+
+.mtw-close-inner {
+  border: 1px solid var(--lp-border-card);
+  border-radius: 1.25rem;
+  background:
+    radial-gradient(
+      120% 140% at 10% 0%,
+      var(--lp-accent-low) 0%,
+      transparent 62%
+    ),
+    var(--mtw-panel);
+  padding: clamp(2rem, 5vw, 3.5rem);
+}
+
+.mtw-close-title {
+  margin: 0;
+  font-family: var(--mtw-mono);
+  font-size: clamp(1.7rem, 3.4vw, 2.35rem);
+  font-weight: 700;
+  letter-spacing: -0.035em;
+  line-height: 1.12;
+  color: var(--lp-text-main);
+}
+
+.mtw-close-lead {
+  margin: 1rem 0 0;
+  max-width: 36rem;
+  font-size: 1rem;
+  line-height: 1.7;
+  color: var(--lp-text-muted);
+}
+
+/* ---------- responsive ---------- */
+
+@media (max-width: 900px) {
+  .mtw-panes {
+    grid-template-columns: 1fr;
+  }
+
+  .mtw-panes-seam span {
+    writing-mode: horizontal-tb;
+    border-left: 0;
+    border-right: 0;
+    border-top: 1px solid var(--lp-border-card);
+    border-bottom: 1px solid var(--lp-border-card);
+    padding: 0.625rem 0.75rem;
+  }
+}
+
+/* Matches NARROW_CAPTURE_QUERY: below this the phone-width captures load, and
+   they are taller than the desktop ones. The same crop is 464px over 432px. */
+@media (max-width: 720px) {
+  .mtw-seam {
+    --mtw-capture-width: 107.407408%;
+
+    aspect-ratio: var(--mtw-ratio-sm);
+    border-radius: clamp(1.125rem, 5.25vw, 2.375rem);
+  }
+}
+
+@media (max-width: 640px) {
+  .mtw-seam-tag {
+    font-size: 0.625rem;
+  }
+}
diff --git a/public/mix-tailwinds/capacity-map-flutter-sm.png b/public/mix-tailwinds/capacity-map-flutter-sm.png
new file mode 100644
index 00000000..adcb6ee5
Binary files /dev/null and b/public/mix-tailwinds/capacity-map-flutter-sm.png differ
diff --git a/public/mix-tailwinds/capacity-map-flutter.png b/public/mix-tailwinds/capacity-map-flutter.png
new file mode 100644
index 00000000..d298a319
Binary files /dev/null and b/public/mix-tailwinds/capacity-map-flutter.png differ
diff --git a/public/mix-tailwinds/capacity-map-tailwind-sm.png b/public/mix-tailwinds/capacity-map-tailwind-sm.png
new file mode 100644
index 00000000..1bf44e6f
Binary files /dev/null and b/public/mix-tailwinds/capacity-map-tailwind-sm.png differ
diff --git a/public/mix-tailwinds/capacity-map-tailwind.png b/public/mix-tailwinds/capacity-map-tailwind.png
new file mode 100644
index 00000000..69a947d8
Binary files /dev/null and b/public/mix-tailwinds/capacity-map-tailwind.png differ
diff --git a/public/mix-tailwinds/incident-room-flutter-sm.png b/public/mix-tailwinds/incident-room-flutter-sm.png
new file mode 100644
index 00000000..c29e485b
Binary files /dev/null and b/public/mix-tailwinds/incident-room-flutter-sm.png differ
diff --git a/public/mix-tailwinds/incident-room-flutter.png b/public/mix-tailwinds/incident-room-flutter.png
new file mode 100644
index 00000000..b9d54a54
Binary files /dev/null and b/public/mix-tailwinds/incident-room-flutter.png differ
diff --git a/public/mix-tailwinds/incident-room-tailwind-sm.png b/public/mix-tailwinds/incident-room-tailwind-sm.png
new file mode 100644
index 00000000..f955dd11
Binary files /dev/null and b/public/mix-tailwinds/incident-room-tailwind-sm.png differ
diff --git a/public/mix-tailwinds/incident-room-tailwind.png b/public/mix-tailwinds/incident-room-tailwind.png
new file mode 100644
index 00000000..063f7aeb
Binary files /dev/null and b/public/mix-tailwinds/incident-room-tailwind.png differ
diff --git a/public/mix-tailwinds/launch-command-flutter-sm.png b/public/mix-tailwinds/launch-command-flutter-sm.png
new file mode 100644
index 00000000..d02bfcf2
Binary files /dev/null and b/public/mix-tailwinds/launch-command-flutter-sm.png differ
diff --git a/public/mix-tailwinds/launch-command-flutter.png b/public/mix-tailwinds/launch-command-flutter.png
new file mode 100644
index 00000000..4e93a9d8
Binary files /dev/null and b/public/mix-tailwinds/launch-command-flutter.png differ
diff --git a/public/mix-tailwinds/launch-command-tailwind-sm.png b/public/mix-tailwinds/launch-command-tailwind-sm.png
new file mode 100644
index 00000000..699b039f
Binary files /dev/null and b/public/mix-tailwinds/launch-command-tailwind-sm.png differ
diff --git a/public/mix-tailwinds/launch-command-tailwind.png b/public/mix-tailwinds/launch-command-tailwind.png
new file mode 100644
index 00000000..a1ff0129
Binary files /dev/null and b/public/mix-tailwinds/launch-command-tailwind.png differ
diff --git a/public/mix-tailwinds/release-timeline-flutter-sm.png b/public/mix-tailwinds/release-timeline-flutter-sm.png
new file mode 100644
index 00000000..447a801f
Binary files /dev/null and b/public/mix-tailwinds/release-timeline-flutter-sm.png differ
diff --git a/public/mix-tailwinds/release-timeline-flutter.png b/public/mix-tailwinds/release-timeline-flutter.png
new file mode 100644
index 00000000..1f326b5d
Binary files /dev/null and b/public/mix-tailwinds/release-timeline-flutter.png differ
diff --git a/public/mix-tailwinds/release-timeline-tailwind-sm.png b/public/mix-tailwinds/release-timeline-tailwind-sm.png
new file mode 100644
index 00000000..c8947620
Binary files /dev/null and b/public/mix-tailwinds/release-timeline-tailwind-sm.png differ
diff --git a/public/mix-tailwinds/release-timeline-tailwind.png b/public/mix-tailwinds/release-timeline-tailwind.png
new file mode 100644
index 00000000..6af81271
Binary files /dev/null and b/public/mix-tailwinds/release-timeline-tailwind.png differ
diff --git a/public/mix-tailwinds/signal-analytics-flutter-sm.png b/public/mix-tailwinds/signal-analytics-flutter-sm.png
new file mode 100644
index 00000000..1091d7d0
Binary files /dev/null and b/public/mix-tailwinds/signal-analytics-flutter-sm.png differ
diff --git a/public/mix-tailwinds/signal-analytics-flutter.png b/public/mix-tailwinds/signal-analytics-flutter.png
new file mode 100644
index 00000000..de27a159
Binary files /dev/null and b/public/mix-tailwinds/signal-analytics-flutter.png differ
diff --git a/public/mix-tailwinds/signal-analytics-tailwind-sm.png b/public/mix-tailwinds/signal-analytics-tailwind-sm.png
new file mode 100644
index 00000000..ef14f346
Binary files /dev/null and b/public/mix-tailwinds/signal-analytics-tailwind-sm.png differ
diff --git a/public/mix-tailwinds/signal-analytics-tailwind.png b/public/mix-tailwinds/signal-analytics-tailwind.png
new file mode 100644
index 00000000..6fc1babe
Binary files /dev/null and b/public/mix-tailwinds/signal-analytics-tailwind.png differ
diff --git a/site-tests/mix-tailwinds-integration.test.mjs b/site-tests/mix-tailwinds-integration.test.mjs
new file mode 100644
index 00000000..80ec1dd6
--- /dev/null
+++ b/site-tests/mix-tailwinds-integration.test.mjs
@@ -0,0 +1,143 @@
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+import test from 'node:test'
+
+const root = process.cwd()
+
+function read(relativePath) {
+  return fs.readFileSync(path.join(root, relativePath), 'utf8')
+}
+
+test('wires the mix_tailwinds page into the landing shell', () => {
+  const route = read('src/app/mix-tailwinds/page.tsx')
+  const landing = read('components/landing/mix-tailwinds/MixTailwindsLanding.tsx')
+  const styles = read('components/landing/mix-tailwinds/mix-tailwinds.css')
+
+  assert.match(route, //)
+  assert.match(route, /Tailwind classes\. Native Flutter\./)
+  assert.match(landing, /className="lp-root mtw-root"/)
+  assert.match(landing, //)
+
+  // Accents live on the page root, not html[data-product]: this is a page
+  // inside the Mix site rather than a separate product in the navbar switcher.
+  assert.match(styles, /\.mtw-root \{[^}]*--lp-accent:/s)
+  const productStyles = read('components/landing/landing.css')
+  assert.doesNotMatch(productStyles, /data-product='mix-tailwinds'/)
+  const layout = read('src/app/layout.jsx')
+  assert.doesNotMatch(layout, /mix-tailwinds/)
+})
+
+test('sells the package instead of reporting a pixel diff', () => {
+  const landing = read('components/landing/mix-tailwinds/MixTailwindsLanding.tsx')
+  const content = read('components/landing/mix-tailwinds/content.ts')
+  const seam = read('components/landing/mix-tailwinds/ComparisonSeam.tsx')
+
+  assert.match(landing, /Get started/)
+  assert.match(landing, /flutter pub add|INSTALL_SNIPPET/)
+  assert.match(content, /flutter pub add mix_tailwinds/)
+
+  // No parity percentages, budgets, or pass/fail language on the page.
+  const pageSource = [landing, content, seam].join('\n')
+  assert.doesNotMatch(pageSource, /diffPercent|toFixed\(2\)|tolerance/i)
+  assert.doesNotMatch(pageSource, /% of pixels/)
+})
+
+test('makes every gallery example a clean browser-to-Flutter comparison', () => {
+  const landing = read('components/landing/mix-tailwinds/MixTailwindsLanding.tsx')
+  const content = read('components/landing/mix-tailwinds/content.ts')
+  const styles = read('components/landing/mix-tailwinds/mix-tailwinds.css')
+
+  assert.match(
+    landing,
+    //s,
+  )
+  assert.doesNotMatch(landing, /Root element|ClassSpecimen|mtw-shot|mtw-gallery-meta/)
+  assert.doesNotMatch(
+    landing,
+    /mtw-hero-stage|mtw-hero-hint| {
+  const landing = read('components/landing/mix-tailwinds/MixTailwindsLanding.tsx')
+
+  assert.match(landing, /aria-controls=\{examplePanelId\(example\.slug\)\}/)
+  assert.match(landing, /tabIndex=\{example\.slug === activeSlug \? 0 : -1\}/)
+  assert.match(landing, /onKeyDown=\{\(event\) => handleExampleKeyDown\(event, index\)\}/)
+  assert.match(landing, /role="tabpanel"/)
+  assert.match(landing, /aria-labelledby=\{exampleTabId\(example\.slug\)\}/)
+  assert.match(landing, /hidden=\{example\.slug !== activeSlug\}/)
+})
+
+test('shows the functional API with real HTML and Dart highlighting', () => {
+  const content = read('components/landing/mix-tailwinds/content.ts')
+  const landing = read('components/landing/mix-tailwinds/MixTailwindsLanding.tsx')
+  const highlighter = read('components/landing/HighlightedCode.tsx')
+
+  // Functional helpers, not the class-based widget constructors.
+  assert.match(content, /DART_SNIPPET = `div\('flex/)
+  assert.match(content, /\n  p\('text-sm/)
+  assert.match(content, /\n  h2\('text-3xl/)
+  assert.match(content, /button\(/)
+  assert.match(content, /onPressed: deploy/)
+  assert.doesNotMatch(content, /Div\(classNames:/)
+
+  assert.match(landing, /lang="html"/)
+  assert.match(landing, /lang="dart"/)
+  assert.match(highlighter, /"html"/)
+  assert.match(highlighter, /"dart"/)
+})
+
+test('serves a legible capture on narrow screens', () => {
+  const content = read('components/landing/mix-tailwinds/content.ts')
+  const seam = read('components/landing/mix-tailwinds/ComparisonSeam.tsx')
+  const styles = read('components/landing/mix-tailwinds/mix-tailwinds.css')
+
+  assert.match(content, /NARROW_CAPTURE_QUERY = "\(max-width: 720px\)"/)
+  assert.match(seam, / {
+  const seam = read('components/landing/mix-tailwinds/ComparisonSeam.tsx')
+
+  assert.match(seam, /type="range"/)
+  assert.match(seam, /aria-labelledby=/)
+  assert.match(seam, /aria-valuetext=/)
+  assert.match(seam, /alt=\{`\$\{example\.title\} rendered in a browser/)
+  assert.match(seam, /alt=\{`\$\{example\.title\} rendered by Flutter/)
+})
diff --git a/src/app/mix-tailwinds/page.tsx b/src/app/mix-tailwinds/page.tsx
new file mode 100644
index 00000000..0e483a73
--- /dev/null
+++ b/src/app/mix-tailwinds/page.tsx
@@ -0,0 +1,28 @@
+import { MixTailwindsLanding } from "../../../components/landing/mix-tailwinds/MixTailwindsLanding";
+
+const description =
+  "Write Tailwind utility classes, render native Flutter widgets. Every example is measured against a real browser rendering the same class string, at three viewport widths.";
+
+export const viewport = {
+  themeColor: "#05040A",
+};
+
+export const metadata = {
+  title: "mix_tailwinds — Tailwind classes. Native Flutter.",
+  description,
+  applicationName: "mix_tailwinds",
+  openGraph: {
+    title: "mix_tailwinds — Tailwind classes, native Flutter widgets",
+    description,
+    type: "website",
+  },
+  twitter: {
+    card: "summary_large_image",
+    title: "mix_tailwinds — Tailwind classes. Native Flutter.",
+    description,
+  },
+};
+
+export default function Page() {
+  return ;
+}