diff --git a/apps/site/src/app/specification/[[...slug]]/page.tsx b/apps/site/src/app/specification/[[...slug]]/page.tsx
index aeeae18dc..80bd30c8d 100644
--- a/apps/site/src/app/specification/[[...slug]]/page.tsx
+++ b/apps/site/src/app/specification/[[...slug]]/page.tsx
@@ -6,6 +6,7 @@ import { DocsBody, DocsDescription, DocsPage } from "fumadocs-ui/layouts/docs/pa
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
+import type { ComponentPropsWithoutRef } from "react";
import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions.tsx";
import { getMDXComponents } from "@/components/mdx.tsx";
import { repoBlobUrl } from "@/components/pdpp-concept/site-facts.ts";
@@ -30,6 +31,14 @@ interface DocsPageProps {
// internals and any direct link) but is no longer the page a visitor lands on.
const ROOT_SLUG_TARGET = ["spec-core"];
+function ResponsiveSpecTable({ children, ...props }: ComponentPropsWithoutRef<"table">) {
+ return (
+
+ );
+}
+
export default async function Page({ params }: DocsPageProps) {
const resolved = await params;
const isRootSlug = !resolved.slug || resolved.slug.length === 0;
@@ -84,6 +93,7 @@ export default async function Page({ params }: DocsPageProps) {
diff --git a/apps/site/src/styles/surfaces/specification.css b/apps/site/src/styles/surfaces/specification.css
index 7e92ee01a..b5122825e 100644
--- a/apps/site/src/styles/surfaces/specification.css
+++ b/apps/site/src/styles/surfaces/specification.css
@@ -82,6 +82,39 @@
margin-inline: 0;
}
+/* The outer `container` supplies the mobile page inset. Fumadocs' own px-4
+ would add a second 1rem inset to the article. */
+@media (max-width: 767.98px) {
+ /* The concept masthead is 4rem on mobile. Fumadocs uses this value as the
+ sticky subnav offset; 3rem let that row overlap the masthead on scroll. */
+ [data-pdpp-doc-theme] #nd-docs-layout {
+ --fd-banner-height: 4rem;
+ --fd-docs-height: calc(100dvh - 4rem);
+ /* The table scrollport outsets through the article's right inset. The grid
+ otherwise clips that extra width before it reaches the outer container. */
+ overflow-x: visible;
+ }
+
+ /* Keep one sticky navigation affordance: the sidebar trigger. The separate
+ TOC popover duplicates that navigation and is the row that crosses the
+ article while scrolling. */
+ [data-pdpp-doc-theme] #nd-subnav {
+ background: var(--pdpp-concept-paper) !important;
+ /* Paint over the table's right-hand bleed without extending the subnav
+ border or changing the position of its controls. */
+ box-shadow: var(--spacing-pad) 0 var(--pdpp-concept-paper);
+ backdrop-filter: none;
+ }
+
+ [data-pdpp-doc-theme] [data-toc-popover] {
+ display: none;
+ }
+
+ [data-pdpp-doc-theme] #nd-page.pdpp-docs-page {
+ padding-inline: 0 !important;
+ }
+}
+
@media (min-width: 768px) {
[data-pdpp-doc-theme] #nd-page.pdpp-docs-page {
--pdpp-docs-gutter: 1.5rem;
@@ -385,11 +418,9 @@
}
[data-pdpp-doc-theme] .pdpp-docs-body table {
- display: block;
width: 100%;
max-width: 100%;
- margin: 24px 0;
- overflow-x: auto;
+ margin: 0;
font-family: var(--font-sans);
font-size: 15px;
font-variant-numeric: tabular-nums;
@@ -399,6 +430,14 @@
border-radius: 0;
}
+[data-pdpp-doc-theme] .pdpp-docs-table-scroll {
+ width: 100%;
+ max-width: 100%;
+ margin: 24px 0;
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+}
+
/* Fumadocs `.prose` paints tables as fd-card boxes; reset so concept ruled
rows inherit paper + ink in both themes. */
[data-pdpp-doc-theme] .pdpp-docs-body.prose :where(table) {
@@ -457,6 +496,70 @@
white-space: nowrap;
}
+/* Tables retain their column model on phones. The default first-column
+ nowrap is useful at reading widths, but on a narrow screen it can consume
+ the table and leave the remaining cells as a thin, clipped strip. */
+@media (max-width: 767.98px) {
+ [data-pdpp-doc-theme] .pdpp-docs-table-scroll {
+ width: calc(100% + var(--spacing-pad));
+ max-width: none;
+ margin-inline-end: calc(-1 * var(--spacing-pad));
+ }
+
+ [data-pdpp-doc-theme] .pdpp-docs-body table {
+ display: table;
+ overflow: visible;
+ table-layout: fixed;
+ }
+
+ /* Three-plus columns retain an 11rem reading measure. The scrollport is
+ visibly outset on the right, signalling that more columns are available. */
+ [data-pdpp-doc-theme]
+ .pdpp-docs-table-scroll
+ > table:has(tr > :nth-child(3)) {
+ width: 33rem;
+ max-width: none;
+ }
+
+ [data-pdpp-doc-theme]
+ .pdpp-docs-table-scroll
+ > table:has(tr > :nth-child(4)) {
+ width: 44rem;
+ }
+
+ [data-pdpp-doc-theme]
+ .pdpp-docs-table-scroll
+ > table:has(tr > :nth-child(5)) {
+ width: 55rem;
+ }
+
+ [data-pdpp-doc-theme]
+ .pdpp-docs-table-scroll
+ > table:has(tr > :nth-child(6)) {
+ width: 66rem;
+ }
+
+ [data-pdpp-doc-theme]
+ .pdpp-docs-table-scroll
+ > table:has(tr > :nth-child(7)) {
+ width: 77rem;
+ }
+
+ [data-pdpp-doc-theme]
+ .pdpp-docs-table-scroll
+ > table:has(tr > :nth-child(8)) {
+ width: 88rem;
+ }
+
+ [data-pdpp-doc-theme] .pdpp-docs-body th:first-child,
+ [data-pdpp-doc-theme] .pdpp-docs-body td:first-child,
+ [data-pdpp-doc-theme] .pdpp-docs-body th :not(pre) > code,
+ [data-pdpp-doc-theme] .pdpp-docs-body td :not(pre) > code {
+ overflow-wrap: anywhere;
+ white-space: normal;
+ }
+}
+
[data-pdpp-doc-theme] .pdpp-docs-body [data-card="true"] {
background: none;
border: none;
@@ -510,6 +613,18 @@
/* ─── Left rail (Fumadocs sidebar slot → spec-rail.tsx) ──────────────────── */
+/* Below the rail's in-flow width the sidebar becomes a fixed drawer
+ (#nd-sidebar-mobile, `invisible fixed`), but fumadocs' grid still reserves
+ its `sidebar` area from --fd-sidebar-width. On a 390px viewport that spent
+ 181px on a rail that is not in flow and left the article 169px, which
+ clipped the h1 mid-word. Zero the reserved column; the drawer and its
+ Open Sidebar trigger are unaffected. */
+@media (max-width: 768px) {
+ [data-pdpp-doc-theme] #nd-docs-layout {
+ --fd-sidebar-width: 0px;
+ }
+}
+
[data-pdpp-doc-theme] #nd-sidebar {
--fd-sidebar-width: var(--spacing-rail);
font-family: var(--font-sans);