function SecurityPage() {
  const items = [
    ['Shield','Tenant isolation','Row-level (tenant_id) + path-prefix on file storage. Physical separation, not just row filtering. Cross-tenant lookups return 404, not 403 — no existence leak.'],
    ['KeyRound','Tokens that behave','JWT access tokens (15 min) + opaque refresh tokens (30 day, DB-backed, rotation mandatory). Refresh replay burns the whole family. JTI blacklist in Redis.'],
    ['Lock','OIDC SSO','Per-tenant connections · PKCE · JWKS cache · enforcement modes (disabled · optional · preferred · required) · break-glass flag is audit-logged at elevated level.'],
    ['FileKey','Secrets at rest','AES-256-GCM AEAD with v-prefix. Reads APP_SECRET_KEY; falls back to SSO_SECRET_KEY. Nothing sensitive touches a log line.'],
    ['Activity','Audit log','19 canonical AuditAction constants. Scrubbed (17-key list), denormalised actor snapshot, tenant-scoped, append-only. Fallback-to-log when storage fails.'],
    ['Database','Bring-your-own-storage','Enterprise plan. Point SyncBuild at your own S3-compatible bucket. Verify-canary probe (write → read → delete) before connection marks Active. Secrets encrypted at rest.'],
    ['Globe','HTTP headers','HSTS · CSP · X-Frame-Options · X-Content-Type-Options · Referrer-Policy on every response via SecurityHeadersListener (priority -100). X-Powered-By and Server stripped.'],
    ['Gauge','Rate limits','Uploads rate-limited 200/h per tenant. Password policy: min 12 chars, letter+digit, not email, not in bundled breached list.'],
    ['FileCheck','Compliance posture','Honest inventory in docs/COMPLIANCE-POSTURE.md — controls we have with file paths, gaps we have with triggers. ISO 27001 · SOC 2 · GDPR · PIPEDA · ISO 19650 · AI governance.'],
  ];
  return (
    <>
      <section className="section-tight pt-16">
        <Container>
          <Eyebrow>Security · trust</Eyebrow>
          <h1 className="m-display mt-3 max-w-3xl">Auth, audit, plan-gating, module-gating, scopes — enforced by the framework, not the controller.</h1>
          <p className="m-lede mt-5 max-w-2xl">Every cross-cutting concern is a route attribute the listener enforces. If a controller forgets the check, the route still can't be hit.</p>
        </Container>
      </section>

      <section className="section">
        <Container>
          <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
            {items.map(it => <FeatureCard key={it[1]} icon={it[0]} title={it[1]}>{it[2]}</FeatureCard>)}
          </div>
        </Container>
      </section>

      <section className="section" style={{ background: 'var(--bg-muted)' }}>
        <Container>
          <SectionHeading eyebrow="Route attributes" title="Three attributes. Three listeners. Deny by default." />
          <div className="card mt-10 overflow-hidden" style={{ fontFamily: 'var(--sb-font-mono)' }}>
            <pre className="p-6 text-[12.5px] leading-relaxed overflow-x-auto">{`#[Route('/api/projects/{id}',
    defaults: [
        '_requires_feature' => 'project_management',   // plan gate
        '_module'           => 'project_management',   // per-tenant module gate
        '_requires_scope'   => 'read:projects',        // OAuth app-token scope
    ],
)]
public function show(string $id): JsonResponse { … }`}</pre>
          </div>
          <p className="m-lede mt-6">PlanGatingListener returns 402 if the tenant plan lacks the feature. TenantModuleGateListener returns 403 if the module isn't enabled. AppScopeListener enforces the scope on app-authenticated requests — deny-by-default, so an ungated endpoint returns 403 to an app token.</p>
        </Container>
      </section>

      <CTABand />
    </>
  );
}
Object.assign(window, { SecurityPage });
