cici 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +122 -0
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +32 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +1886 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/api/asset/[...path]/route.js +1 -0
- package/.next/server/app/api/asset/[...path]/route.js.nft.json +1 -0
- package/.next/server/app/api/auth/[...nextauth]/route.js +1 -0
- package/.next/server/app/api/auth/[...nextauth]/route.js.nft.json +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/comments/[cid]/reactions/route.js +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/comments/[cid]/reactions/route.js.nft.json +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/comments/[cid]/route.js +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/comments/[cid]/route.js.nft.json +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/replies/route.js +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/replies/route.js.nft.json +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/resolve/route.js +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/resolve/route.js.nft.json +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/route.js +1 -0
- package/.next/server/app/api/blog/[id]/highlights/[hid]/route.js.nft.json +1 -0
- package/.next/server/app/api/blog/[id]/highlights/route.js +1 -0
- package/.next/server/app/api/blog/[id]/highlights/route.js.nft.json +1 -0
- package/.next/server/app/api/graphql/route.js +140 -0
- package/.next/server/app/api/graphql/route.js.nft.json +1 -0
- package/.next/server/app/api/mile/prompts/route.js +230 -0
- package/.next/server/app/api/mile/prompts/route.js.nft.json +1 -0
- package/.next/server/app/api/mile/prompts.body +1 -0
- package/.next/server/app/api/mile/prompts.meta +1 -0
- package/.next/server/app/api/upload/route.js +1 -0
- package/.next/server/app/api/upload/route.js.nft.json +1 -0
- package/.next/server/app/api/webhooks/notification/route.js +1 -0
- package/.next/server/app/api/webhooks/notification/route.js.nft.json +1 -0
- package/.next/server/app/atom.xml/route.js +34 -0
- package/.next/server/app/atom.xml/route.js.nft.json +1 -0
- package/.next/server/app/atom.xml.body +15 -0
- package/.next/server/app/atom.xml.meta +1 -0
- package/.next/server/app/blog/[id]/page.js +30 -0
- package/.next/server/app/blog/[id]/page.js.nft.json +1 -0
- package/.next/server/app/blog/[id]/page_client-reference-manifest.js +1 -0
- package/.next/server/app/blog/page.js +1 -0
- package/.next/server/app/blog/page.js.nft.json +1 -0
- package/.next/server/app/blog/page_client-reference-manifest.js +1 -0
- package/.next/server/app/editor/page.js +45 -0
- package/.next/server/app/editor/page.js.nft.json +1 -0
- package/.next/server/app/editor/page_client-reference-manifest.js +1 -0
- package/.next/server/app/favicon.ico/route.js +1 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/memos/page.js +26 -0
- package/.next/server/app/memos/page.js.nft.json +1 -0
- package/.next/server/app/memos/page_client-reference-manifest.js +1 -0
- package/.next/server/app/page.js +1 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/robots.txt/route.js +1 -0
- package/.next/server/app/robots.txt/route.js.nft.json +1 -0
- package/.next/server/app/robots.txt.body +8 -0
- package/.next/server/app/robots.txt.meta +1 -0
- package/.next/server/app/rss.xml/route.js +18 -0
- package/.next/server/app/rss.xml/route.js.nft.json +1 -0
- package/.next/server/app/rss.xml.body +12 -0
- package/.next/server/app/rss.xml.meta +1 -0
- package/.next/server/app/sitemap.xml/route.js +76 -0
- package/.next/server/app/sitemap.xml/route.js.nft.json +1 -0
- package/.next/server/app/sitemap.xml.body +33 -0
- package/.next/server/app/sitemap.xml.meta +1 -0
- package/.next/server/app/unavailable/page.js +1 -0
- package/.next/server/app/unavailable/page.js.nft.json +1 -0
- package/.next/server/app/unavailable/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +27 -0
- package/.next/server/chunks/1230.js +1 -0
- package/.next/server/chunks/1446.js +1 -0
- package/.next/server/chunks/1450.js +1 -0
- package/.next/server/chunks/1536.js +1 -0
- package/.next/server/chunks/1615.js +1 -0
- package/.next/server/chunks/1682.js +6 -0
- package/.next/server/chunks/179.js +14 -0
- package/.next/server/chunks/2448.js +34 -0
- package/.next/server/chunks/2481.js +1 -0
- package/.next/server/chunks/2518.js +12 -0
- package/.next/server/chunks/2581.js +1 -0
- package/.next/server/chunks/2878.js +63 -0
- package/.next/server/chunks/3125.js +1 -0
- package/.next/server/chunks/330.js +1 -0
- package/.next/server/chunks/3381.js +1 -0
- package/.next/server/chunks/3464.js +1 -0
- package/.next/server/chunks/3573.js +1 -0
- package/.next/server/chunks/3607.js +14 -0
- package/.next/server/chunks/3844.js +1 -0
- package/.next/server/chunks/4536.js +1 -0
- package/.next/server/chunks/4742.js +1 -0
- package/.next/server/chunks/4857.js +1 -0
- package/.next/server/chunks/6621.js +1 -0
- package/.next/server/chunks/7070.js +12 -0
- package/.next/server/chunks/719.js +1 -0
- package/.next/server/chunks/7667.js +1 -0
- package/.next/server/chunks/7674.js +1 -0
- package/.next/server/chunks/7937.js +1 -0
- package/.next/server/chunks/7990.js +1 -0
- package/.next/server/chunks/8038.js +1 -0
- package/.next/server/chunks/8195.js +1 -0
- package/.next/server/chunks/8198.js +1 -0
- package/.next/server/chunks/8224.js +1 -0
- package/.next/server/chunks/8398.js +1 -0
- package/.next/server/chunks/8948.js +2 -0
- package/.next/server/chunks/9600.js +1 -0
- package/.next/server/chunks/9627.js +1 -0
- package/.next/server/chunks/9715.js +1 -0
- package/.next/server/chunks/9772.js +2 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/functions-config-manifest.json +1 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +1 -1
- package/.next/standalone/.next/server/app/api/mile/prompts.body +1 -1
- package/.next/standalone/.next/server/app/atom.xml.body +1 -1
- package/.next/standalone/.next/server/app/rss.xml.body +1 -1
- package/.next/standalone/.next/server/app/sitemap.xml.body +5 -5
- package/.next/standalone/.next/server/app-paths-manifest.json +3 -3
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/package.json +5 -4
- package/.next/static/PDn4f_-4CIbfG8EmueowG/_buildManifest.js +1 -0
- package/.next/static/PDn4f_-4CIbfG8EmueowG/_ssgManifest.js +1 -0
- package/.next/static/chunks/0e5ce63c-dc8fe6cd5df6dc6f.js +1 -0
- package/.next/static/chunks/0e762574-561b753e40bf5566.js +1 -0
- package/.next/static/chunks/117-70c87e31cd233c14.js +2 -0
- package/.next/static/chunks/152-16409649f7f42f9f.js +1 -0
- package/.next/static/chunks/153-f9c1aae3150e3029.js +1 -0
- package/.next/static/chunks/164-27f8b4187a29c395.js +1 -0
- package/.next/static/chunks/306-8d9c9a07b04a69f4.js +1 -0
- package/.next/static/chunks/343-ef3404804cc77cf3.js +1 -0
- package/.next/static/chunks/37-4aae6f4051a21b05.js +1 -0
- package/.next/static/chunks/479ba886-f76cebda33235b4d.js +1 -0
- package/.next/static/chunks/586-51f7e768b82803f6.js +1 -0
- package/.next/static/chunks/654-13e1845df8edb304.js +1 -0
- package/.next/static/chunks/767-2ec5d3ea9fe43cfa.js +1 -0
- package/.next/static/chunks/789-9810818d7be29598.js +1 -0
- package/.next/static/chunks/795d4814-090707ad3f792458.js +1 -0
- package/.next/static/chunks/878-b71984a1ef1b61e8.js +1 -0
- package/.next/static/chunks/8e1d74a4-f0365ca0a1823520.js +1 -0
- package/.next/static/chunks/961-29616c8a08e9f439.js +1 -0
- package/.next/static/chunks/972-774fbff93f73f32d.js +1 -0
- package/.next/static/chunks/app/_not-found/page-3061d7e79b77cf51.js +1 -0
- package/.next/static/chunks/app/blog/[id]/page-1fc779790ce384a8.js +1 -0
- package/.next/static/chunks/app/blog/page-096c29c6f69ebc33.js +1 -0
- package/.next/static/chunks/app/editor/page-f387899e39bc5e66.js +1 -0
- package/.next/static/chunks/app/layout-3439dc235234d73d.js +1 -0
- package/.next/static/chunks/app/login/page-6b91892f0e29c5e9.js +1 -0
- package/.next/static/chunks/app/memos/page-1b33cb7e1f301ba0.js +1 -0
- package/.next/static/chunks/app/page-f7dabeade6df80fd.js +1 -0
- package/.next/static/chunks/app/unavailable/page-c7774081778f08fc.js +1 -0
- package/.next/static/chunks/c916193b-16ee67ea0eb0c9df.js +1 -0
- package/.next/static/chunks/d3ac728e-709e52b22b85fd0a.js +1 -0
- package/.next/static/chunks/ee560e2c-0aeb447bc68f09f5.js +1 -0
- package/.next/static/chunks/fd9d1056-a4afdface6e53879.js +1 -0
- package/.next/static/chunks/framework-00a8ba1a63cfdc9e.js +1 -0
- package/.next/static/chunks/main-530e55a5659ae7e4.js +1 -0
- package/.next/static/chunks/main-app-838433dc18720d83.js +1 -0
- package/.next/static/chunks/pages/_app-15e2daefa259f0b5.js +1 -0
- package/.next/static/chunks/pages/_error-28b803cb2479b966.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-a3d722a538b58757.js +1 -0
- package/.next/static/css/3864b451a61e4546.css +1 -0
- package/.next/static/css/67e4cfd62ab3f75c.css +1 -0
- package/.next/static/css/bf9d0ede562bd061.css +3 -0
- package/.next/static/media/04b170fa1114a606-s.woff2 +0 -0
- package/.next/static/media/08e5276c49c9ea3c-s.woff2 +0 -0
- package/.next/static/media/095b3ef40b322df1-s.woff2 +0 -0
- package/.next/static/media/0d843c40cdb24da5-s.woff2 +0 -0
- package/.next/static/media/16198ffb9c9fdd71-s.woff2 +0 -0
- package/.next/static/media/185e1e5dfa43e25f-s.woff2 +0 -0
- package/.next/static/media/1cfb11966f8bf838-s.woff2 +0 -0
- package/.next/static/media/1f4ef3a7563c157c-s.woff2 +0 -0
- package/.next/static/media/207ad64b019bcdef-s.woff2 +0 -0
- package/.next/static/media/21f0145a081cb2c8-s.woff2 +0 -0
- package/.next/static/media/25169183b7610496-s.woff2 +0 -0
- package/.next/static/media/2c913da5dc1da2ce-s.woff2 +0 -0
- package/.next/static/media/2d7ca78015d280d1-s.woff2 +0 -0
- package/.next/static/media/32be0b7a9127e345-s.woff2 +0 -0
- package/.next/static/media/33a4c9c744d0f9cd-s.woff2 +0 -0
- package/.next/static/media/3579da1a17de9863-s.woff2 +0 -0
- package/.next/static/media/36524af73d313f3b-s.woff2 +0 -0
- package/.next/static/media/3683934951fcdfff-s.woff2 +0 -0
- package/.next/static/media/3a3a2937d60ce37f-s.woff2 +0 -0
- package/.next/static/media/3e2c1db9f0de322f-s.woff2 +0 -0
- package/.next/static/media/3f282b8fe3ef7004-s.woff2 +0 -0
- package/.next/static/media/4294bde93a0ebb03-s.woff2 +0 -0
- package/.next/static/media/4cb92749e68500bf-s.woff2 +0 -0
- package/.next/static/media/4d3ec3b19c6bf06c-s.woff2 +0 -0
- package/.next/static/media/4eaaaae34a570973-s.woff2 +0 -0
- package/.next/static/media/4f45b9473df2758b-s.woff2 +0 -0
- package/.next/static/media/59769944ee073575-s.woff2 +0 -0
- package/.next/static/media/5bca2cb7d28055ae-s.woff2 +0 -0
- package/.next/static/media/5d217fec32e2068e-s.woff2 +0 -0
- package/.next/static/media/5f46c2de3a30070d-s.woff2 +0 -0
- package/.next/static/media/651d2e07f2cec067-s.woff2 +0 -0
- package/.next/static/media/684008be259a59c6-s.woff2 +0 -0
- package/.next/static/media/6858bbe75ab53fba-s.woff2 +0 -0
- package/.next/static/media/6cbcf0df7f219406-s.woff2 +0 -0
- package/.next/static/media/6d92d05365182413-s.woff2 +0 -0
- package/.next/static/media/7470b994d8569922-s.woff2 +0 -0
- package/.next/static/media/74e7e1e3fdad08dd-s.woff2 +0 -0
- package/.next/static/media/77901e7a9559d10c-s.woff2 +0 -0
- package/.next/static/media/796fab757a0e6146-s.woff2 +0 -0
- package/.next/static/media/7a830ebca9fe9db7-s.woff2 +0 -0
- package/.next/static/media/7bef3958b14cb6e3-s.woff2 +0 -0
- package/.next/static/media/81eb003b78a42a7b-s.woff2 +0 -0
- package/.next/static/media/84057adca98c0827-s.woff2 +0 -0
- package/.next/static/media/84a87fc9f232641e-s.woff2 +0 -0
- package/.next/static/media/84efce35a48f395c-s.woff2 +0 -0
- package/.next/static/media/872bfc82854f88b2-s.woff2 +0 -0
- package/.next/static/media/878380e8db69cb31-s.woff2 +0 -0
- package/.next/static/media/89c333b5a8239870-s.woff2 +0 -0
- package/.next/static/media/8a2e82a5f6677757-s.woff2 +0 -0
- package/.next/static/media/8cb8df5532e281af-s.woff2 +0 -0
- package/.next/static/media/8efbe4984052b206-s.woff2 +0 -0
- package/.next/static/media/8f2ab7576a12de97-s.woff2 +0 -0
- package/.next/static/media/905b23b26fa2645b-s.woff2 +0 -0
- package/.next/static/media/91fb5c9b9b43e9ec-s.woff2 +0 -0
- package/.next/static/media/92079ce87aa6c00d-s.woff2 +0 -0
- package/.next/static/media/9d87bcf0c05a83d6-s.p.woff2 +0 -0
- package/.next/static/media/9e9c82ad03b7dfa5-s.woff2 +0 -0
- package/.next/static/media/KaTeX_AMS-Regular.1608a09b.woff +0 -0
- package/.next/static/media/KaTeX_AMS-Regular.4aafdb68.ttf +0 -0
- package/.next/static/media/KaTeX_AMS-Regular.a79f1c31.woff2 +0 -0
- package/.next/static/media/KaTeX_Caligraphic-Bold.b6770918.woff +0 -0
- package/.next/static/media/KaTeX_Caligraphic-Bold.cce5b8ec.ttf +0 -0
- package/.next/static/media/KaTeX_Caligraphic-Bold.ec17d132.woff2 +0 -0
- package/.next/static/media/KaTeX_Caligraphic-Regular.07ef19e7.ttf +0 -0
- package/.next/static/media/KaTeX_Caligraphic-Regular.55fac258.woff2 +0 -0
- package/.next/static/media/KaTeX_Caligraphic-Regular.dad44a7f.woff +0 -0
- package/.next/static/media/KaTeX_Fraktur-Bold.9f256b85.woff +0 -0
- package/.next/static/media/KaTeX_Fraktur-Bold.b18f59e1.ttf +0 -0
- package/.next/static/media/KaTeX_Fraktur-Bold.d42a5579.woff2 +0 -0
- package/.next/static/media/KaTeX_Fraktur-Regular.7c187121.woff +0 -0
- package/.next/static/media/KaTeX_Fraktur-Regular.d3c882a6.woff2 +0 -0
- package/.next/static/media/KaTeX_Fraktur-Regular.ed38e79f.ttf +0 -0
- package/.next/static/media/KaTeX_Main-Bold.b74a1a8b.ttf +0 -0
- package/.next/static/media/KaTeX_Main-Bold.c3fb5ac2.woff2 +0 -0
- package/.next/static/media/KaTeX_Main-Bold.d181c465.woff +0 -0
- package/.next/static/media/KaTeX_Main-BoldItalic.6f2bb1df.woff2 +0 -0
- package/.next/static/media/KaTeX_Main-BoldItalic.70d8b0a5.ttf +0 -0
- package/.next/static/media/KaTeX_Main-BoldItalic.e3f82f9d.woff +0 -0
- package/.next/static/media/KaTeX_Main-Italic.47373d1e.ttf +0 -0
- package/.next/static/media/KaTeX_Main-Italic.8916142b.woff2 +0 -0
- package/.next/static/media/KaTeX_Main-Italic.9024d815.woff +0 -0
- package/.next/static/media/KaTeX_Main-Regular.0462f03b.woff2 +0 -0
- package/.next/static/media/KaTeX_Main-Regular.7f51fe03.woff +0 -0
- package/.next/static/media/KaTeX_Main-Regular.b7f8fe9b.ttf +0 -0
- package/.next/static/media/KaTeX_Math-BoldItalic.572d331f.woff2 +0 -0
- package/.next/static/media/KaTeX_Math-BoldItalic.a879cf83.ttf +0 -0
- package/.next/static/media/KaTeX_Math-BoldItalic.f1035d8d.woff +0 -0
- package/.next/static/media/KaTeX_Math-Italic.5295ba48.woff +0 -0
- package/.next/static/media/KaTeX_Math-Italic.939bc644.ttf +0 -0
- package/.next/static/media/KaTeX_Math-Italic.f28c23ac.woff2 +0 -0
- package/.next/static/media/KaTeX_SansSerif-Bold.8c5b5494.woff2 +0 -0
- package/.next/static/media/KaTeX_SansSerif-Bold.94e1e8dc.ttf +0 -0
- package/.next/static/media/KaTeX_SansSerif-Bold.bf59d231.woff +0 -0
- package/.next/static/media/KaTeX_SansSerif-Italic.3b1e59b3.woff2 +0 -0
- package/.next/static/media/KaTeX_SansSerif-Italic.7c9bc82b.woff +0 -0
- package/.next/static/media/KaTeX_SansSerif-Italic.b4c20c84.ttf +0 -0
- package/.next/static/media/KaTeX_SansSerif-Regular.74048478.woff +0 -0
- package/.next/static/media/KaTeX_SansSerif-Regular.ba21ed5f.woff2 +0 -0
- package/.next/static/media/KaTeX_SansSerif-Regular.d4d7ba48.ttf +0 -0
- package/.next/static/media/KaTeX_Script-Regular.03e9641d.woff2 +0 -0
- package/.next/static/media/KaTeX_Script-Regular.07505710.woff +0 -0
- package/.next/static/media/KaTeX_Script-Regular.fe9cbbe1.ttf +0 -0
- package/.next/static/media/KaTeX_Size1-Regular.e1e279cb.woff +0 -0
- package/.next/static/media/KaTeX_Size1-Regular.eae34984.woff2 +0 -0
- package/.next/static/media/KaTeX_Size1-Regular.fabc004a.ttf +0 -0
- package/.next/static/media/KaTeX_Size2-Regular.57727022.woff +0 -0
- package/.next/static/media/KaTeX_Size2-Regular.5916a24f.woff2 +0 -0
- package/.next/static/media/KaTeX_Size2-Regular.d6b476ec.ttf +0 -0
- package/.next/static/media/KaTeX_Size3-Regular.9acaf01c.woff +0 -0
- package/.next/static/media/KaTeX_Size3-Regular.a144ef58.ttf +0 -0
- package/.next/static/media/KaTeX_Size3-Regular.b4230e7e.woff2 +0 -0
- package/.next/static/media/KaTeX_Size4-Regular.10d95fd3.woff2 +0 -0
- package/.next/static/media/KaTeX_Size4-Regular.7a996c9d.woff +0 -0
- package/.next/static/media/KaTeX_Size4-Regular.fbccdabe.ttf +0 -0
- package/.next/static/media/KaTeX_Typewriter-Regular.6258592b.woff +0 -0
- package/.next/static/media/KaTeX_Typewriter-Regular.a8709e36.woff2 +0 -0
- package/.next/static/media/KaTeX_Typewriter-Regular.d97aaf4a.ttf +0 -0
- package/.next/static/media/a0c7b168290800ea-s.woff2 +0 -0
- package/.next/static/media/a211c214b387705f-s.woff2 +0 -0
- package/.next/static/media/a515f80c15233dd2-s.woff2 +0 -0
- package/.next/static/media/a59cf4e3704a8bf9-s.woff2 +0 -0
- package/.next/static/media/a7107da688a6a6c4-s.woff2 +0 -0
- package/.next/static/media/b0785a8aafa8fb40-s.woff2 +0 -0
- package/.next/static/media/b3afeb402d0641d1-s.woff2 +0 -0
- package/.next/static/media/b3d4f23ea9869e41-s.woff2 +0 -0
- package/.next/static/media/b88b6f09060fd402-s.woff2 +0 -0
- package/.next/static/media/b9bde58fd979694e-s.woff2 +0 -0
- package/.next/static/media/bdb722ed5812b325-s.woff2 +0 -0
- package/.next/static/media/c24e4778556eba42-s.woff2 +0 -0
- package/.next/static/media/c3040269b1771b02-s.woff2 +0 -0
- package/.next/static/media/c4123b44ca624496-s.woff2 +0 -0
- package/.next/static/media/ccadbccc98acfb1c-s.woff2 +0 -0
- package/.next/static/media/d04351f5bc3644ea-s.woff2 +0 -0
- package/.next/static/media/d1778a980b3fad78-s.woff2 +0 -0
- package/.next/static/media/d416562cb7cba055-s.woff2 +0 -0
- package/.next/static/media/d872fd07230782c0-s.woff2 +0 -0
- package/.next/static/media/db3791bbb1ae5747-s.woff2 +0 -0
- package/.next/static/media/dd090f323c5887c3-s.woff2 +0 -0
- package/.next/static/media/dd1fcf6432702b11-s.woff2 +0 -0
- package/.next/static/media/e45abf9e3abcfe63-s.woff2 +0 -0
- package/.next/static/media/e6e70594dba816d3-s.woff2 +0 -0
- package/.next/static/media/e8a6cb3b0c8c1d41-s.woff2 +0 -0
- package/.next/static/media/e8fdc46fb8ed5f75-s.woff2 +0 -0
- package/.next/static/media/ed7a78808c74aaab-s.woff2 +0 -0
- package/.next/static/media/ef9a93445fe49953-s.woff2 +0 -0
- package/.next/static/media/f00fd2cfeda7750f-s.woff2 +0 -0
- package/.next/static/media/f0863c4be7cb812e-s.woff2 +0 -0
- package/.next/static/media/f2fb250c4baa1fea-s.woff2 +0 -0
- package/.next/static/media/f392a13a0094c3d1-s.woff2 +0 -0
- package/.next/static/media/f42f4d2ecc3cba08-s.woff2 +0 -0
- package/.next/static/media/f8f98f7e33dde58b-s.woff2 +0 -0
- package/.next/static/media/fba9d66c2358f8e6-s.woff2 +0 -0
- package/.next/static/media/fc8b52876cd8aa42-s.woff2 +0 -0
- package/.next/static/media/fcc5ccbd071c5f3e-s.woff2 +0 -0
- package/.next/static/media/fe9908567bfd9cbf-s.woff2 +0 -0
- package/.next/trace +120 -0
- package/.next/types/app/api/asset/[...path]/route.ts +343 -0
- package/.next/types/app/api/auth/[...nextauth]/route.ts +343 -0
- package/.next/types/app/api/blog/[id]/highlights/[hid]/comments/[cid]/reactions/route.ts +343 -0
- package/.next/types/app/api/blog/[id]/highlights/[hid]/comments/[cid]/route.ts +343 -0
- package/.next/types/app/api/blog/[id]/highlights/[hid]/replies/route.ts +343 -0
- package/.next/types/app/api/blog/[id]/highlights/[hid]/resolve/route.ts +343 -0
- package/.next/types/app/api/blog/[id]/highlights/[hid]/route.ts +343 -0
- package/.next/types/app/api/blog/[id]/highlights/route.ts +343 -0
- package/.next/types/app/api/graphql/route.ts +343 -0
- package/.next/types/app/api/mile/prompts/route.ts +343 -0
- package/.next/types/app/api/upload/route.ts +343 -0
- package/.next/types/app/api/webhooks/notification/route.ts +343 -0
- package/.next/types/app/atom.xml/route.ts +343 -0
- package/.next/types/app/blog/[id]/page.ts +79 -0
- package/.next/types/app/blog/page.ts +79 -0
- package/.next/types/app/editor/page.ts +79 -0
- package/.next/types/app/login/page.ts +79 -0
- package/.next/types/app/memos/page.ts +79 -0
- package/.next/types/app/page.ts +79 -0
- package/.next/types/app/rss.xml/route.ts +343 -0
- package/.next/types/app/unavailable/page.ts +79 -0
- package/.next/types/package.json +1 -0
- package/bin/cici.js +9 -7
- package/package.json +5 -4
- package/public/Tinymind-banner.png +0 -0
- package/public/Tinymind.dmg +0 -0
- package/public/icon-144.jpg +0 -0
- package/public/icon.jpg +0 -0
- package/public/manifest.json +25 -0
- package/public/presentations/coordinated-access-control-with-policy/index.html +1593 -0
- package/public/tinymind.icns +0 -0
- /package/.next/standalone/.next/static/{Pl18p4h2shbbqjypPxStc → PDn4f_-4CIbfG8EmueowG}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Pl18p4h2shbbqjypPxStc → PDn4f_-4CIbfG8EmueowG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,1593 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Coordinated Access Control with @policy — Minghe Huang · Field Notes</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Newsreader:ital,opsz,wght@0,6..72,300..700;1,6..72,300..700&family=JetBrains+Mono:wght@400;500;700&display=swap">
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
/* Day Edition */
|
|
13
|
+
--paper: #f5efe4;
|
|
14
|
+
--paper-2: #ebe3d4;
|
|
15
|
+
--paper-shade: #ddd3c0;
|
|
16
|
+
--ink: #1a1816;
|
|
17
|
+
--ink-2: #5c554a;
|
|
18
|
+
--rule: #1a1816;
|
|
19
|
+
--rule-soft: #c5b9a0;
|
|
20
|
+
--accent: #a4243b;
|
|
21
|
+
--accent-deep: #6b1726;
|
|
22
|
+
--signal: #c69a30;
|
|
23
|
+
--good: #4a6f3d;
|
|
24
|
+
--warn: #a4243b;
|
|
25
|
+
--code-bg: #ebe3d4;
|
|
26
|
+
--noise-opacity: 0.045;
|
|
27
|
+
}
|
|
28
|
+
[data-theme="dark"] {
|
|
29
|
+
/* Night Edition */
|
|
30
|
+
--paper: #161310;
|
|
31
|
+
--paper-2: #1f1b16;
|
|
32
|
+
--paper-shade: #25201a;
|
|
33
|
+
--ink: #f0e8d8;
|
|
34
|
+
--ink-2: #8a8074;
|
|
35
|
+
--rule: #f0e8d8;
|
|
36
|
+
--rule-soft: #3a342a;
|
|
37
|
+
--accent: #d96978;
|
|
38
|
+
--accent-deep: #b04a5b;
|
|
39
|
+
--signal: #e8c547;
|
|
40
|
+
--good: #93b67c;
|
|
41
|
+
--warn: #d96978;
|
|
42
|
+
--code-bg: #1f1b16;
|
|
43
|
+
--noise-opacity: 0.03;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
47
|
+
html, body { height: 100%; overflow-x: hidden; }
|
|
48
|
+
body {
|
|
49
|
+
font-family: 'Newsreader', 'Iowan Old Style', 'Charter', Georgia, serif;
|
|
50
|
+
font-feature-settings: 'tnum' 1, 'kern' 1, 'liga' 1, 'onum' 1;
|
|
51
|
+
font-variation-settings: 'opsz' 22;
|
|
52
|
+
background: var(--paper);
|
|
53
|
+
color: var(--ink);
|
|
54
|
+
font-size: 18px;
|
|
55
|
+
line-height: 1.55;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
transition: background 0.4s ease, color 0.4s ease;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ---------- Type / spacing tokens (viewport-relative) ---------- */
|
|
61
|
+
:root {
|
|
62
|
+
--slide-pad-x: clamp(1.6rem, 5.6vw, 5rem);
|
|
63
|
+
--slide-pad-top: clamp(1rem, 3vh, 2.4rem);
|
|
64
|
+
--slide-pad-bottom: clamp(2.4rem, 4vh, 3.4rem);
|
|
65
|
+
--content-gap: clamp(0.4rem, 1.4vh, 1rem);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ---------- Newsprint texture ---------- */
|
|
69
|
+
body::before {
|
|
70
|
+
content: "";
|
|
71
|
+
position: fixed;
|
|
72
|
+
inset: 0;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
z-index: 0;
|
|
75
|
+
opacity: var(--noise-opacity);
|
|
76
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.55 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
|
|
77
|
+
mix-blend-mode: multiply;
|
|
78
|
+
}
|
|
79
|
+
[data-theme="dark"] body::before { mix-blend-mode: screen; }
|
|
80
|
+
|
|
81
|
+
/* ---------- Masthead removed ---------- */
|
|
82
|
+
|
|
83
|
+
/* ---------- Deck container ---------- */
|
|
84
|
+
#deck { position: relative; height: 100vh; height: 100dvh; width: 100vw; }
|
|
85
|
+
|
|
86
|
+
.slide {
|
|
87
|
+
position: absolute;
|
|
88
|
+
top: 0; left: 0;
|
|
89
|
+
width: 100%;
|
|
90
|
+
height: 100vh;
|
|
91
|
+
height: 100dvh;
|
|
92
|
+
padding: var(--slide-pad-top) var(--slide-pad-x) var(--slide-pad-bottom);
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
opacity: 0;
|
|
97
|
+
pointer-events: none;
|
|
98
|
+
transition: opacity 0.42s ease;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
z-index: 1;
|
|
101
|
+
}
|
|
102
|
+
.slide.active { opacity: 1; pointer-events: auto; z-index: 2; }
|
|
103
|
+
|
|
104
|
+
/* ---------- Stagger reveal ---------- */
|
|
105
|
+
@keyframes paperFade {
|
|
106
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
107
|
+
to { opacity: 1; transform: translateY(0); }
|
|
108
|
+
}
|
|
109
|
+
@keyframes ruleSweep {
|
|
110
|
+
from { transform: scaleX(0); }
|
|
111
|
+
to { transform: scaleX(1); }
|
|
112
|
+
}
|
|
113
|
+
.slide.active > * { animation: paperFade 0.55s ease-out backwards; }
|
|
114
|
+
.slide.active > *:nth-child(1) { animation-delay: 0.05s; }
|
|
115
|
+
.slide.active > *:nth-child(2) { animation-delay: 0.13s; }
|
|
116
|
+
.slide.active > *:nth-child(3) { animation-delay: 0.21s; }
|
|
117
|
+
.slide.active > *:nth-child(4) { animation-delay: 0.29s; }
|
|
118
|
+
.slide.active > *:nth-child(5) { animation-delay: 0.37s; }
|
|
119
|
+
.slide.active > *:nth-child(n+6) { animation-delay: 0.45s; }
|
|
120
|
+
|
|
121
|
+
/* ---------- Kicker (eyebrow) ---------- */
|
|
122
|
+
.kicker {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: baseline;
|
|
125
|
+
gap: 0.9rem;
|
|
126
|
+
margin-bottom: 1rem;
|
|
127
|
+
font-family: 'Fraunces', Georgia, serif;
|
|
128
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
129
|
+
font-size: 0.7rem;
|
|
130
|
+
letter-spacing: 0.36em;
|
|
131
|
+
text-transform: uppercase;
|
|
132
|
+
color: var(--accent);
|
|
133
|
+
}
|
|
134
|
+
.kicker::before {
|
|
135
|
+
content: "";
|
|
136
|
+
width: 1.6rem;
|
|
137
|
+
height: 2px;
|
|
138
|
+
background: var(--accent);
|
|
139
|
+
display: inline-block;
|
|
140
|
+
transform-origin: left;
|
|
141
|
+
animation: ruleSweep 0.7s 0.1s ease-out backwards;
|
|
142
|
+
}
|
|
143
|
+
.kicker .folio {
|
|
144
|
+
margin-left: auto;
|
|
145
|
+
color: var(--ink-2);
|
|
146
|
+
font-variation-settings: 'opsz' 9, 'wght' 500;
|
|
147
|
+
letter-spacing: 0.28em;
|
|
148
|
+
font-feature-settings: 'tnum' 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ---------- Headlines ---------- */
|
|
152
|
+
.slide h1 {
|
|
153
|
+
font-family: 'Fraunces', Georgia, serif;
|
|
154
|
+
font-variation-settings: 'opsz' 144, 'wght' 700;
|
|
155
|
+
font-size: clamp(2.2rem, 4.6vw, 4rem);
|
|
156
|
+
line-height: 0.96;
|
|
157
|
+
letter-spacing: -0.025em;
|
|
158
|
+
color: var(--ink);
|
|
159
|
+
margin-bottom: 1.2rem;
|
|
160
|
+
}
|
|
161
|
+
.slide h2 {
|
|
162
|
+
font-family: 'Fraunces', Georgia, serif;
|
|
163
|
+
font-variation-settings: 'opsz' 96, 'wght' 600;
|
|
164
|
+
font-size: clamp(1.5rem, 2.7vw, 2.4rem);
|
|
165
|
+
line-height: 1.05;
|
|
166
|
+
letter-spacing: -0.022em;
|
|
167
|
+
color: var(--ink);
|
|
168
|
+
margin-bottom: 0.9rem;
|
|
169
|
+
max-width: 22ch;
|
|
170
|
+
}
|
|
171
|
+
.slide h2 em {
|
|
172
|
+
color: var(--accent);
|
|
173
|
+
font-style: italic;
|
|
174
|
+
font-variation-settings: 'opsz' 96, 'wght' 600;
|
|
175
|
+
}
|
|
176
|
+
.slide h3 {
|
|
177
|
+
font-family: 'Fraunces', Georgia, serif;
|
|
178
|
+
font-variation-settings: 'opsz' 36, 'wght' 600;
|
|
179
|
+
font-size: clamp(1.1rem, 1.7vw, 1.4rem);
|
|
180
|
+
margin-top: 1.4rem;
|
|
181
|
+
margin-bottom: 0.55rem;
|
|
182
|
+
color: var(--ink);
|
|
183
|
+
letter-spacing: -0.01em;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ---------- Body text ---------- */
|
|
187
|
+
.slide p, .slide li {
|
|
188
|
+
font-family: 'Newsreader', Georgia, serif;
|
|
189
|
+
font-variation-settings: 'opsz' 22;
|
|
190
|
+
font-size: clamp(1.02rem, 1.4vw, 1.22rem);
|
|
191
|
+
line-height: 1.55;
|
|
192
|
+
color: var(--ink);
|
|
193
|
+
}
|
|
194
|
+
.slide p { margin-bottom: 0.95rem; max-width: 72ch; }
|
|
195
|
+
.slide ul { margin-left: 1.4em; margin-bottom: 0.95rem; max-width: 72ch; list-style: none; }
|
|
196
|
+
.slide ul li {
|
|
197
|
+
position: relative;
|
|
198
|
+
margin-bottom: 0.5rem;
|
|
199
|
+
padding-left: 0.4em;
|
|
200
|
+
}
|
|
201
|
+
.slide ul li::before {
|
|
202
|
+
content: "¶";
|
|
203
|
+
position: absolute;
|
|
204
|
+
left: -1.2em;
|
|
205
|
+
color: var(--accent);
|
|
206
|
+
font-family: 'Fraunces', serif;
|
|
207
|
+
font-variation-settings: 'wght' 500;
|
|
208
|
+
font-size: 0.95em;
|
|
209
|
+
}
|
|
210
|
+
.slide .muted { color: var(--ink-2); font-style: italic; }
|
|
211
|
+
.slide .accent { color: var(--accent); font-weight: 600; font-style: normal; }
|
|
212
|
+
.slide .warn { color: var(--accent); font-weight: 600; }
|
|
213
|
+
.slide .good { color: var(--good); font-weight: 600; }
|
|
214
|
+
.slide strong { color: var(--ink); font-weight: 700; }
|
|
215
|
+
|
|
216
|
+
/* ---------- Inline code ---------- */
|
|
217
|
+
.slide code:not(pre code) {
|
|
218
|
+
font-family: 'JetBrains Mono', SFMono-Regular, Menlo, monospace;
|
|
219
|
+
font-size: 0.88em;
|
|
220
|
+
background: var(--paper-2);
|
|
221
|
+
padding: 0.08em 0.42em;
|
|
222
|
+
border: 1px solid var(--rule-soft);
|
|
223
|
+
border-radius: 2px;
|
|
224
|
+
color: var(--ink);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* Inline code inside headlines: drop the box, keep the monospace font, GraphQL pink */
|
|
228
|
+
.slide h1 code:not(pre code), .slide h2 code:not(pre code) {
|
|
229
|
+
background: transparent;
|
|
230
|
+
border: none;
|
|
231
|
+
padding: 0;
|
|
232
|
+
border-radius: 0;
|
|
233
|
+
font-size: 0.82em;
|
|
234
|
+
color: #E10098;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ---------- Code block as "Exhibit" ---------- */
|
|
238
|
+
.slide pre {
|
|
239
|
+
position: relative;
|
|
240
|
+
background: var(--paper-2);
|
|
241
|
+
color: var(--ink);
|
|
242
|
+
padding: 1.2rem 1.4rem 1rem;
|
|
243
|
+
font-family: 'JetBrains Mono', SFMono-Regular, Menlo, monospace;
|
|
244
|
+
font-size: clamp(0.78rem, 1vw, 0.96rem);
|
|
245
|
+
line-height: 1.55;
|
|
246
|
+
overflow-x: auto;
|
|
247
|
+
border-top: 2px solid var(--rule);
|
|
248
|
+
border-bottom: 2px solid var(--rule);
|
|
249
|
+
border-radius: 0;
|
|
250
|
+
margin: 0.7rem 0;
|
|
251
|
+
box-shadow: none;
|
|
252
|
+
}
|
|
253
|
+
.slide pre::before {
|
|
254
|
+
content: "Exhibit · Verbatim";
|
|
255
|
+
position: absolute;
|
|
256
|
+
top: -0.62rem;
|
|
257
|
+
left: 1.4rem;
|
|
258
|
+
background: var(--paper);
|
|
259
|
+
padding: 0 0.6rem;
|
|
260
|
+
font-family: 'Fraunces', serif;
|
|
261
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
262
|
+
font-size: 0.6rem;
|
|
263
|
+
letter-spacing: 0.32em;
|
|
264
|
+
text-transform: uppercase;
|
|
265
|
+
color: var(--accent);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* ---------- Blockquote ---------- */
|
|
269
|
+
.slide blockquote {
|
|
270
|
+
border-left: 3px solid var(--accent);
|
|
271
|
+
padding: 0.4rem 0 0.4rem 1.2rem;
|
|
272
|
+
margin: 1rem 0;
|
|
273
|
+
font-family: 'Newsreader', serif;
|
|
274
|
+
font-style: italic;
|
|
275
|
+
font-variation-settings: 'opsz' 36;
|
|
276
|
+
color: var(--ink);
|
|
277
|
+
font-size: clamp(1.1rem, 1.7vw, 1.4rem);
|
|
278
|
+
max-width: 70ch;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ---------- TITLE slide (front page) ---------- */
|
|
282
|
+
.title-slide {
|
|
283
|
+
align-items: stretch;
|
|
284
|
+
justify-content: flex-start;
|
|
285
|
+
padding-top: clamp(3vh, 6vh, 8vh);
|
|
286
|
+
padding-bottom: clamp(3.5rem, 6vh, 5rem);
|
|
287
|
+
}
|
|
288
|
+
.title-slide .frontpage-rule {
|
|
289
|
+
position: relative;
|
|
290
|
+
display: flex;
|
|
291
|
+
justify-content: space-between;
|
|
292
|
+
align-items: center;
|
|
293
|
+
border-top: 4px solid var(--rule);
|
|
294
|
+
border-bottom: 1px solid var(--rule);
|
|
295
|
+
padding: 0.5rem 0.2rem;
|
|
296
|
+
margin-bottom: 2.6rem;
|
|
297
|
+
font-family: 'Fraunces', serif;
|
|
298
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
299
|
+
font-size: 0.7rem;
|
|
300
|
+
letter-spacing: 0.34em;
|
|
301
|
+
text-transform: uppercase;
|
|
302
|
+
color: var(--ink);
|
|
303
|
+
}
|
|
304
|
+
.title-slide .frontpage-rule .vol { color: var(--ink-2); }
|
|
305
|
+
.title-slide .frontpage-rule .price { color: var(--accent); }
|
|
306
|
+
.title-slide h1 {
|
|
307
|
+
font-size: clamp(3.4rem, 9vw, 7rem);
|
|
308
|
+
line-height: 0.92;
|
|
309
|
+
letter-spacing: -0.035em;
|
|
310
|
+
margin-bottom: 1.2rem;
|
|
311
|
+
max-width: 14ch;
|
|
312
|
+
font-variation-settings: 'opsz' 144, 'wght' 800;
|
|
313
|
+
}
|
|
314
|
+
.title-slide h1 .ampersand {
|
|
315
|
+
font-style: italic;
|
|
316
|
+
color: var(--accent);
|
|
317
|
+
font-variation-settings: 'opsz' 144, 'wght' 400;
|
|
318
|
+
}
|
|
319
|
+
.title-slide .deck {
|
|
320
|
+
font-family: 'Newsreader', serif;
|
|
321
|
+
font-variation-settings: 'opsz' 36, 'wght' 400;
|
|
322
|
+
font-style: italic;
|
|
323
|
+
font-size: clamp(1.3rem, 2.2vw, 1.85rem);
|
|
324
|
+
color: var(--ink-2);
|
|
325
|
+
margin-bottom: 2.4rem;
|
|
326
|
+
max-width: 38ch;
|
|
327
|
+
line-height: 1.32;
|
|
328
|
+
}
|
|
329
|
+
.title-slide .byline {
|
|
330
|
+
display: flex;
|
|
331
|
+
align-items: baseline;
|
|
332
|
+
gap: 1.5rem;
|
|
333
|
+
border-top: 1px solid var(--rule);
|
|
334
|
+
padding-top: 1rem;
|
|
335
|
+
margin-top: auto;
|
|
336
|
+
font-family: 'Fraunces', serif;
|
|
337
|
+
font-variation-settings: 'opsz' 36, 'wght' 600;
|
|
338
|
+
font-size: 0.95rem;
|
|
339
|
+
letter-spacing: 0.04em;
|
|
340
|
+
}
|
|
341
|
+
.title-slide .byline .name {
|
|
342
|
+
color: var(--ink);
|
|
343
|
+
font-variation-settings: 'opsz' 36, 'wght' 700;
|
|
344
|
+
font-size: 1.05rem;
|
|
345
|
+
}
|
|
346
|
+
.title-slide .byline .meta {
|
|
347
|
+
font-family: 'Newsreader', serif;
|
|
348
|
+
font-style: italic;
|
|
349
|
+
font-variation-settings: 'opsz' 22, 'wght' 400;
|
|
350
|
+
color: var(--ink-2);
|
|
351
|
+
letter-spacing: 0;
|
|
352
|
+
}
|
|
353
|
+
.title-slide .byline .reading-time {
|
|
354
|
+
margin-left: auto;
|
|
355
|
+
font-family: 'Fraunces', serif;
|
|
356
|
+
font-variation-settings: 'opsz' 9, 'wght' 600;
|
|
357
|
+
font-size: 0.66rem;
|
|
358
|
+
letter-spacing: 0.32em;
|
|
359
|
+
text-transform: uppercase;
|
|
360
|
+
color: var(--accent);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ---------- Two-column layout ---------- */
|
|
364
|
+
.two-col {
|
|
365
|
+
display: grid;
|
|
366
|
+
grid-template-columns: 1fr 1fr;
|
|
367
|
+
gap: 2.6rem;
|
|
368
|
+
align-items: start;
|
|
369
|
+
margin-top: 1rem;
|
|
370
|
+
position: relative;
|
|
371
|
+
}
|
|
372
|
+
.two-col::before {
|
|
373
|
+
content: "";
|
|
374
|
+
position: absolute;
|
|
375
|
+
top: 0; bottom: 0; left: 50%;
|
|
376
|
+
width: 1px;
|
|
377
|
+
background: var(--rule-soft);
|
|
378
|
+
transform: translateX(-50%);
|
|
379
|
+
}
|
|
380
|
+
.col h3 { margin-top: 0; }
|
|
381
|
+
.col-head {
|
|
382
|
+
font-family: 'Fraunces', serif;
|
|
383
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
384
|
+
font-size: 0.7rem;
|
|
385
|
+
letter-spacing: 0.34em;
|
|
386
|
+
text-transform: uppercase;
|
|
387
|
+
margin-bottom: 0.9rem;
|
|
388
|
+
padding-bottom: 0.5rem;
|
|
389
|
+
border-bottom: 2px solid currentColor;
|
|
390
|
+
display: inline-block;
|
|
391
|
+
}
|
|
392
|
+
.col-head.before { color: var(--accent); }
|
|
393
|
+
.col-head.after { color: var(--good); }
|
|
394
|
+
|
|
395
|
+
/* ---------- Callout ---------- */
|
|
396
|
+
.callout {
|
|
397
|
+
background: transparent;
|
|
398
|
+
border: none;
|
|
399
|
+
border-top: 1px solid var(--rule);
|
|
400
|
+
border-bottom: 1px solid var(--rule);
|
|
401
|
+
padding: 0.7rem 0;
|
|
402
|
+
margin: 0.9rem 0 0.4rem;
|
|
403
|
+
font-family: 'Newsreader', serif;
|
|
404
|
+
font-variation-settings: 'opsz' 36;
|
|
405
|
+
font-style: italic;
|
|
406
|
+
font-size: clamp(0.95rem, 1.35vw, 1.18rem);
|
|
407
|
+
line-height: 1.4;
|
|
408
|
+
max-width: 76ch;
|
|
409
|
+
color: var(--ink);
|
|
410
|
+
position: relative;
|
|
411
|
+
}
|
|
412
|
+
.callout::before {
|
|
413
|
+
content: "❝";
|
|
414
|
+
position: absolute;
|
|
415
|
+
left: -1.4rem;
|
|
416
|
+
top: 0.2rem;
|
|
417
|
+
font-family: 'Fraunces', serif;
|
|
418
|
+
font-size: 1.7rem;
|
|
419
|
+
color: var(--accent);
|
|
420
|
+
font-style: normal;
|
|
421
|
+
}
|
|
422
|
+
.callout strong { color: var(--accent); font-weight: 700; }
|
|
423
|
+
|
|
424
|
+
/* ---------- KPI grid (ledger) ---------- */
|
|
425
|
+
.kpi-grid {
|
|
426
|
+
display: grid;
|
|
427
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
428
|
+
gap: 0;
|
|
429
|
+
margin: 0.9rem 0;
|
|
430
|
+
border-top: 2px solid var(--rule);
|
|
431
|
+
border-bottom: 2px solid var(--rule);
|
|
432
|
+
}
|
|
433
|
+
.kpi {
|
|
434
|
+
padding: 1rem 1.2rem 0.9rem;
|
|
435
|
+
border-right: 1px solid var(--rule-soft);
|
|
436
|
+
background: transparent;
|
|
437
|
+
border-radius: 0;
|
|
438
|
+
}
|
|
439
|
+
.kpi:last-child { border-right: none; }
|
|
440
|
+
.kpi .num {
|
|
441
|
+
font-family: 'Fraunces', serif;
|
|
442
|
+
font-variation-settings: 'opsz' 144, 'wght' 700;
|
|
443
|
+
font-size: clamp(1.6rem, 2.8vw, 2.6rem);
|
|
444
|
+
color: var(--accent);
|
|
445
|
+
line-height: 0.95;
|
|
446
|
+
letter-spacing: -0.04em;
|
|
447
|
+
margin-bottom: 0.4rem;
|
|
448
|
+
font-feature-settings: 'tnum' 1, 'lnum' 1;
|
|
449
|
+
}
|
|
450
|
+
.kpi .label {
|
|
451
|
+
font-family: 'Newsreader', serif;
|
|
452
|
+
font-variation-settings: 'opsz' 14;
|
|
453
|
+
font-style: italic;
|
|
454
|
+
font-size: clamp(0.78rem, 0.95vw, 0.92rem);
|
|
455
|
+
color: var(--ink-2);
|
|
456
|
+
line-height: 1.3;
|
|
457
|
+
max-width: 26ch;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* ---------- Six stakeholder briefs ---------- */
|
|
461
|
+
.stakeholders {
|
|
462
|
+
counter-reset: stake;
|
|
463
|
+
display: grid;
|
|
464
|
+
grid-template-columns: repeat(2, 1fr);
|
|
465
|
+
gap: 0;
|
|
466
|
+
margin: 0.5rem 0 0;
|
|
467
|
+
border-top: 2px solid var(--rule);
|
|
468
|
+
}
|
|
469
|
+
.stake {
|
|
470
|
+
counter-increment: stake;
|
|
471
|
+
position: relative;
|
|
472
|
+
padding: 0.7rem 1.2rem 0.75rem 0;
|
|
473
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
474
|
+
border-right: 1px solid var(--rule-soft);
|
|
475
|
+
background: transparent;
|
|
476
|
+
}
|
|
477
|
+
.stake:nth-child(2n) { padding-right: 0; padding-left: 1.2rem; border-right: none; }
|
|
478
|
+
.stake:nth-last-child(-n+2) { border-bottom: none; }
|
|
479
|
+
.stake::before {
|
|
480
|
+
content: counter(stake, decimal-leading-zero);
|
|
481
|
+
position: absolute;
|
|
482
|
+
top: 0.7rem;
|
|
483
|
+
right: 0;
|
|
484
|
+
font-family: 'Fraunces', serif;
|
|
485
|
+
font-variation-settings: 'opsz' 144, 'wght' 300;
|
|
486
|
+
font-style: italic;
|
|
487
|
+
font-size: 1.2rem;
|
|
488
|
+
color: var(--accent);
|
|
489
|
+
font-feature-settings: 'lnum' 1;
|
|
490
|
+
}
|
|
491
|
+
.stake:nth-child(2n)::before { right: auto; left: 0; top: 0.7rem; }
|
|
492
|
+
.stake:nth-child(2n) .who, .stake:nth-child(2n) .claim { padding-left: 1.9rem; }
|
|
493
|
+
.stake:nth-child(odd) .who, .stake:nth-child(odd) .claim { padding-right: 1.9rem; }
|
|
494
|
+
.stake .who {
|
|
495
|
+
font-family: 'Fraunces', serif;
|
|
496
|
+
font-variation-settings: 'opsz' 36, 'wght' 700;
|
|
497
|
+
font-size: clamp(0.92rem, 1.2vw, 1.08rem);
|
|
498
|
+
color: var(--ink);
|
|
499
|
+
margin-bottom: 0.25rem;
|
|
500
|
+
letter-spacing: -0.005em;
|
|
501
|
+
line-height: 1.2;
|
|
502
|
+
}
|
|
503
|
+
.stake .claim {
|
|
504
|
+
color: var(--ink-2);
|
|
505
|
+
font-family: 'Newsreader', serif;
|
|
506
|
+
font-size: clamp(0.82rem, 1vw, 0.94rem);
|
|
507
|
+
line-height: 1.4;
|
|
508
|
+
}
|
|
509
|
+
.stake .claim strong { color: var(--accent); font-weight: 600; }
|
|
510
|
+
.stake .claim em {
|
|
511
|
+
font-style: italic;
|
|
512
|
+
color: var(--ink);
|
|
513
|
+
}
|
|
514
|
+
.stake-head {
|
|
515
|
+
font-family: 'Fraunces', serif;
|
|
516
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
517
|
+
font-size: 0.66rem;
|
|
518
|
+
letter-spacing: 0.32em;
|
|
519
|
+
text-transform: uppercase;
|
|
520
|
+
color: var(--accent);
|
|
521
|
+
padding: 0.7rem 1.2rem 0.55rem 0;
|
|
522
|
+
border-bottom: 2px solid var(--rule);
|
|
523
|
+
border-right: 1px solid var(--rule-soft);
|
|
524
|
+
background: transparent;
|
|
525
|
+
}
|
|
526
|
+
.stake-head.right-col {
|
|
527
|
+
padding: 0.7rem 0 0.55rem 1.2rem;
|
|
528
|
+
border-right: none;
|
|
529
|
+
color: var(--ink-2);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* ---------- Architecture flow (typewriter) ---------- */
|
|
533
|
+
.arch-flow {
|
|
534
|
+
font-family: 'JetBrains Mono', SFMono-Regular, Menlo, monospace;
|
|
535
|
+
background: var(--paper-2);
|
|
536
|
+
padding: 1.1rem 1.4rem 1rem;
|
|
537
|
+
margin: 0.7rem 0 0.7rem;
|
|
538
|
+
font-size: clamp(0.7rem, 0.95vw, 0.92rem);
|
|
539
|
+
line-height: 1.5;
|
|
540
|
+
border-top: 2px solid var(--rule);
|
|
541
|
+
border-bottom: 2px solid var(--rule);
|
|
542
|
+
border-radius: 0;
|
|
543
|
+
position: relative;
|
|
544
|
+
white-space: pre;
|
|
545
|
+
overflow-x: auto;
|
|
546
|
+
}
|
|
547
|
+
.arch-flow::before {
|
|
548
|
+
content: "Trace · Verbatim";
|
|
549
|
+
position: absolute;
|
|
550
|
+
top: -0.62rem;
|
|
551
|
+
left: 1.4rem;
|
|
552
|
+
background: var(--paper);
|
|
553
|
+
padding: 0 0.6rem;
|
|
554
|
+
font-family: 'Fraunces', serif;
|
|
555
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
556
|
+
font-size: 0.6rem;
|
|
557
|
+
letter-spacing: 0.32em;
|
|
558
|
+
text-transform: uppercase;
|
|
559
|
+
color: var(--accent);
|
|
560
|
+
}
|
|
561
|
+
.arch-flow .hop { color: var(--ink-2); }
|
|
562
|
+
.arch-flow .step { color: var(--ink); font-weight: 500; }
|
|
563
|
+
.arch-flow .pol { color: var(--accent); font-weight: 600; }
|
|
564
|
+
.arch-flow .ok { color: var(--good); font-weight: 700; }
|
|
565
|
+
.arch-flow .bad { color: var(--accent); font-weight: 700; }
|
|
566
|
+
.arch-flow .label { color: var(--ink-2); font-style: italic; }
|
|
567
|
+
|
|
568
|
+
/* ---------- Architecture diagram (slide 6, no border/label) ---------- */
|
|
569
|
+
.arch-diagram {
|
|
570
|
+
font-family: 'JetBrains Mono', SFMono-Regular, Menlo, monospace;
|
|
571
|
+
background: transparent;
|
|
572
|
+
padding: 0.6rem 0;
|
|
573
|
+
margin: 0.8rem 0 0;
|
|
574
|
+
font-size: clamp(0.65rem, 0.85vw, 0.82rem);
|
|
575
|
+
line-height: 1.45;
|
|
576
|
+
white-space: pre;
|
|
577
|
+
overflow-x: auto;
|
|
578
|
+
color: var(--ink);
|
|
579
|
+
}
|
|
580
|
+
.arch-diagram .step { color: var(--ink); font-weight: 600; }
|
|
581
|
+
.arch-diagram .label { color: var(--ink-2); font-style: italic; }
|
|
582
|
+
.arch-diagram .accent { color: var(--accent); font-weight: 600; }
|
|
583
|
+
.arch-diagram .hop { color: var(--ink-2); }
|
|
584
|
+
.arch-diagram .ok { color: var(--good); font-weight: 700; }
|
|
585
|
+
.arch-diagram .bad { color: var(--accent); font-weight: 700; }
|
|
586
|
+
|
|
587
|
+
/* ---------- Coordination layers (ledger table) ---------- */
|
|
588
|
+
.layers {
|
|
589
|
+
display: grid;
|
|
590
|
+
grid-template-columns: 0.8fr 1.2fr 1fr;
|
|
591
|
+
gap: 0;
|
|
592
|
+
margin: 0.9rem 0;
|
|
593
|
+
border-top: 2px solid var(--rule);
|
|
594
|
+
border-bottom: 2px solid var(--rule);
|
|
595
|
+
}
|
|
596
|
+
.layers .h, .layers .c {
|
|
597
|
+
padding: 0.75rem 1.1rem;
|
|
598
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
599
|
+
font-size: clamp(0.88rem, 1.15vw, 1.05rem);
|
|
600
|
+
border-right: 1px solid var(--rule-soft);
|
|
601
|
+
}
|
|
602
|
+
.layers .c:nth-child(3n), .layers .h:nth-child(3n) { border-right: none; }
|
|
603
|
+
.layers > div:nth-last-child(-n+3) { border-bottom: none; }
|
|
604
|
+
.layers .h {
|
|
605
|
+
background: transparent;
|
|
606
|
+
color: var(--accent);
|
|
607
|
+
font-family: 'Fraunces', serif;
|
|
608
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
609
|
+
font-size: 0.66rem;
|
|
610
|
+
letter-spacing: 0.32em;
|
|
611
|
+
text-transform: uppercase;
|
|
612
|
+
border-bottom: 2px solid var(--rule);
|
|
613
|
+
padding-bottom: 0.6rem;
|
|
614
|
+
}
|
|
615
|
+
.layers .c.l-name {
|
|
616
|
+
font-family: 'Fraunces', serif;
|
|
617
|
+
font-variation-settings: 'opsz' 36, 'wght' 700;
|
|
618
|
+
color: var(--ink);
|
|
619
|
+
font-size: clamp(0.95rem, 1.3vw, 1.15rem);
|
|
620
|
+
}
|
|
621
|
+
.layers .c.l-owner {
|
|
622
|
+
color: var(--accent);
|
|
623
|
+
font-weight: 500;
|
|
624
|
+
font-family: 'Newsreader', serif;
|
|
625
|
+
}
|
|
626
|
+
.layers .c.l-cadence {
|
|
627
|
+
color: var(--ink-2);
|
|
628
|
+
font-style: italic;
|
|
629
|
+
font-family: 'Newsreader', serif;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* ---------- Chaos grid (slide 4: classified-ad style) ---------- */
|
|
633
|
+
.chaos-grid {
|
|
634
|
+
display: grid;
|
|
635
|
+
grid-template-columns: repeat(3, 1fr);
|
|
636
|
+
gap: 0;
|
|
637
|
+
margin: 0.7rem 0;
|
|
638
|
+
border-top: 2px solid var(--rule);
|
|
639
|
+
border-bottom: 2px solid var(--rule);
|
|
640
|
+
}
|
|
641
|
+
.chaos-grid .box {
|
|
642
|
+
padding: 0.85rem 1rem 0.85rem;
|
|
643
|
+
font-family: 'JetBrains Mono', monospace;
|
|
644
|
+
font-size: clamp(0.78rem, 1vw, 0.95rem);
|
|
645
|
+
text-align: left;
|
|
646
|
+
background: transparent;
|
|
647
|
+
border-right: 1px solid var(--rule-soft);
|
|
648
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
649
|
+
position: relative;
|
|
650
|
+
color: var(--ink);
|
|
651
|
+
}
|
|
652
|
+
.chaos-grid .box:nth-child(3n) { border-right: none; }
|
|
653
|
+
.chaos-grid .box:nth-last-child(-n+3) { border-bottom: none; }
|
|
654
|
+
.chaos-grid .box::after {
|
|
655
|
+
content: "✕";
|
|
656
|
+
position: absolute;
|
|
657
|
+
top: 0.55rem;
|
|
658
|
+
right: 0.7rem;
|
|
659
|
+
color: var(--accent);
|
|
660
|
+
font-size: 0.65rem;
|
|
661
|
+
font-family: 'Fraunces', serif;
|
|
662
|
+
}
|
|
663
|
+
.chaos-grid .box .who {
|
|
664
|
+
font-family: 'Fraunces', serif;
|
|
665
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
666
|
+
color: var(--accent);
|
|
667
|
+
font-size: 0.6rem;
|
|
668
|
+
letter-spacing: 0.3em;
|
|
669
|
+
text-transform: uppercase;
|
|
670
|
+
margin-bottom: 0.4rem;
|
|
671
|
+
padding-bottom: 0.3rem;
|
|
672
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/* ---------- Timeline (numbered stages) ---------- */
|
|
676
|
+
.timeline {
|
|
677
|
+
display: grid;
|
|
678
|
+
grid-template-columns: auto 1fr;
|
|
679
|
+
gap: 0.7rem 1.4rem;
|
|
680
|
+
margin: 0.8rem 0;
|
|
681
|
+
align-items: baseline;
|
|
682
|
+
border-top: 2px solid var(--rule);
|
|
683
|
+
padding-top: 0.9rem;
|
|
684
|
+
}
|
|
685
|
+
.timeline .stage {
|
|
686
|
+
font-family: 'Fraunces', serif;
|
|
687
|
+
font-variation-settings: 'opsz' 144, 'wght' 700;
|
|
688
|
+
font-style: italic;
|
|
689
|
+
font-size: clamp(1.2rem, 1.9vw, 1.7rem);
|
|
690
|
+
color: var(--accent);
|
|
691
|
+
text-align: right;
|
|
692
|
+
white-space: nowrap;
|
|
693
|
+
letter-spacing: -0.02em;
|
|
694
|
+
font-feature-settings: 'lnum' 1;
|
|
695
|
+
min-width: 4.5rem;
|
|
696
|
+
}
|
|
697
|
+
.timeline > div:nth-child(2n) {
|
|
698
|
+
font-family: 'Newsreader', serif;
|
|
699
|
+
font-variation-settings: 'opsz' 22;
|
|
700
|
+
color: var(--ink);
|
|
701
|
+
font-size: clamp(0.92rem, 1.2vw, 1.1rem);
|
|
702
|
+
line-height: 1.45;
|
|
703
|
+
padding-bottom: 0.6rem;
|
|
704
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
705
|
+
}
|
|
706
|
+
.timeline > div:nth-last-child(1) { border-bottom: none; padding-bottom: 0; }
|
|
707
|
+
|
|
708
|
+
/* ---------- Lessons (editorial column) ---------- */
|
|
709
|
+
.lessons {
|
|
710
|
+
display: grid;
|
|
711
|
+
grid-template-columns: 1fr;
|
|
712
|
+
gap: 0;
|
|
713
|
+
margin-top: 0.7rem;
|
|
714
|
+
border-top: 2px solid var(--rule);
|
|
715
|
+
}
|
|
716
|
+
.lesson {
|
|
717
|
+
padding: 0.85rem 0 0.95rem;
|
|
718
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
719
|
+
display: grid;
|
|
720
|
+
grid-template-columns: 7rem 1fr;
|
|
721
|
+
gap: 1.5rem;
|
|
722
|
+
align-items: start;
|
|
723
|
+
}
|
|
724
|
+
.lesson:last-child { border-bottom: none; }
|
|
725
|
+
.lesson .num {
|
|
726
|
+
font-family: 'Fraunces', serif;
|
|
727
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
728
|
+
font-size: 0.66rem;
|
|
729
|
+
letter-spacing: 0.34em;
|
|
730
|
+
text-transform: uppercase;
|
|
731
|
+
color: var(--accent);
|
|
732
|
+
padding-top: 0.25rem;
|
|
733
|
+
}
|
|
734
|
+
.lesson .num::after {
|
|
735
|
+
content: "";
|
|
736
|
+
display: block;
|
|
737
|
+
width: 1.4rem;
|
|
738
|
+
height: 2px;
|
|
739
|
+
background: var(--accent);
|
|
740
|
+
margin-top: 0.4rem;
|
|
741
|
+
}
|
|
742
|
+
.lesson .head {
|
|
743
|
+
font-family: 'Fraunces', serif;
|
|
744
|
+
font-variation-settings: 'opsz' 96, 'wght' 600;
|
|
745
|
+
font-size: clamp(1.05rem, 1.65vw, 1.4rem);
|
|
746
|
+
line-height: 1.18;
|
|
747
|
+
margin-bottom: 0.3rem;
|
|
748
|
+
color: var(--ink);
|
|
749
|
+
letter-spacing: -0.015em;
|
|
750
|
+
}
|
|
751
|
+
.lesson .body {
|
|
752
|
+
font-family: 'Newsreader', serif;
|
|
753
|
+
font-variation-settings: 'opsz' 22;
|
|
754
|
+
font-size: clamp(0.88rem, 1.1vw, 1.02rem);
|
|
755
|
+
color: var(--ink-2);
|
|
756
|
+
line-height: 1.45;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/* ---------- Verdict cards (slide 15) ---------- */
|
|
760
|
+
.attempts {
|
|
761
|
+
display: grid;
|
|
762
|
+
grid-template-columns: 1fr;
|
|
763
|
+
gap: 0;
|
|
764
|
+
margin: 0.7rem 0 0.4rem;
|
|
765
|
+
border-top: 2px solid var(--rule);
|
|
766
|
+
}
|
|
767
|
+
.attempt {
|
|
768
|
+
padding: 0.85rem 0 0.95rem;
|
|
769
|
+
display: grid;
|
|
770
|
+
grid-template-columns: 4.5rem 1fr 7rem;
|
|
771
|
+
gap: 1.4rem;
|
|
772
|
+
align-items: center;
|
|
773
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
774
|
+
}
|
|
775
|
+
.attempt:last-child { border-bottom: none; }
|
|
776
|
+
.attempt .ver {
|
|
777
|
+
font-family: 'Fraunces', serif;
|
|
778
|
+
font-variation-settings: 'opsz' 144, 'wght' 700;
|
|
779
|
+
font-style: italic;
|
|
780
|
+
font-size: clamp(1.7rem, 2.8vw, 2.4rem);
|
|
781
|
+
color: var(--accent);
|
|
782
|
+
letter-spacing: -0.04em;
|
|
783
|
+
font-feature-settings: 'lnum' 1;
|
|
784
|
+
}
|
|
785
|
+
.attempt .desc {
|
|
786
|
+
font-family: 'Newsreader', serif;
|
|
787
|
+
font-variation-settings: 'opsz' 22;
|
|
788
|
+
font-size: clamp(0.92rem, 1.2vw, 1.08rem);
|
|
789
|
+
line-height: 1.45;
|
|
790
|
+
color: var(--ink);
|
|
791
|
+
}
|
|
792
|
+
.attempt .verdict {
|
|
793
|
+
font-family: 'Fraunces', serif;
|
|
794
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
795
|
+
font-size: 0.7rem;
|
|
796
|
+
letter-spacing: 0.32em;
|
|
797
|
+
text-transform: uppercase;
|
|
798
|
+
padding: 0.55rem 0.8rem;
|
|
799
|
+
text-align: center;
|
|
800
|
+
border: 2px solid currentColor;
|
|
801
|
+
white-space: nowrap;
|
|
802
|
+
}
|
|
803
|
+
.attempt.fail .verdict { color: var(--accent); }
|
|
804
|
+
.attempt.win .verdict { color: var(--good); background: var(--good); color: var(--paper); border-color: var(--good); }
|
|
805
|
+
.attempt.fail .ver { color: var(--accent); }
|
|
806
|
+
.attempt.win .ver { color: var(--good); }
|
|
807
|
+
|
|
808
|
+
/* ---------- Big-text editorial pullquote ---------- */
|
|
809
|
+
.big-text {
|
|
810
|
+
font-family: 'Fraunces', serif;
|
|
811
|
+
font-variation-settings: 'opsz' 144, 'wght' 400;
|
|
812
|
+
font-style: italic;
|
|
813
|
+
font-size: clamp(1.8rem, 3.6vw, 3.1rem);
|
|
814
|
+
line-height: 1.18;
|
|
815
|
+
letter-spacing: -0.022em;
|
|
816
|
+
max-width: 22ch;
|
|
817
|
+
margin-bottom: 1.6rem;
|
|
818
|
+
color: var(--ink);
|
|
819
|
+
}
|
|
820
|
+
.big-text .accent {
|
|
821
|
+
color: var(--accent);
|
|
822
|
+
font-style: normal;
|
|
823
|
+
font-variation-settings: 'opsz' 144, 'wght' 700;
|
|
824
|
+
}
|
|
825
|
+
.big-text .warn {
|
|
826
|
+
color: var(--ink-2);
|
|
827
|
+
font-style: normal;
|
|
828
|
+
text-decoration: line-through;
|
|
829
|
+
text-decoration-thickness: 3px;
|
|
830
|
+
text-decoration-color: var(--accent);
|
|
831
|
+
text-decoration-skip-ink: none;
|
|
832
|
+
}
|
|
833
|
+
.big-text em {
|
|
834
|
+
font-style: italic;
|
|
835
|
+
color: var(--ink);
|
|
836
|
+
font-variation-settings: 'opsz' 144, 'wght' 500;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/* Slide 6 / 12 specific */
|
|
840
|
+
.insight-mark {
|
|
841
|
+
font-family: 'Fraunces', serif;
|
|
842
|
+
font-style: italic;
|
|
843
|
+
font-size: clamp(3rem, 5vw, 4.5rem);
|
|
844
|
+
color: var(--accent);
|
|
845
|
+
line-height: 1;
|
|
846
|
+
margin-bottom: 0.5rem;
|
|
847
|
+
display: block;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/* ---------- Closing slide ---------- */
|
|
851
|
+
.closing-page {
|
|
852
|
+
justify-content: space-between;
|
|
853
|
+
}
|
|
854
|
+
.closing-page .lede { margin-top: 1rem; }
|
|
855
|
+
.closing-page .signoff {
|
|
856
|
+
margin-top: auto;
|
|
857
|
+
padding-top: 1.4rem;
|
|
858
|
+
border-top: 1px solid var(--rule);
|
|
859
|
+
display: flex;
|
|
860
|
+
justify-content: space-between;
|
|
861
|
+
align-items: baseline;
|
|
862
|
+
font-family: 'Fraunces', serif;
|
|
863
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
864
|
+
font-size: 0.7rem;
|
|
865
|
+
letter-spacing: 0.32em;
|
|
866
|
+
text-transform: uppercase;
|
|
867
|
+
color: var(--ink-2);
|
|
868
|
+
}
|
|
869
|
+
.closing-page .signoff .right { color: var(--accent); }
|
|
870
|
+
|
|
871
|
+
/* ---------- Footer chrome ---------- */
|
|
872
|
+
footer.deck-chrome {
|
|
873
|
+
position: fixed;
|
|
874
|
+
bottom: 0; left: 0; right: 0;
|
|
875
|
+
display: flex;
|
|
876
|
+
justify-content: flex-end;
|
|
877
|
+
align-items: center;
|
|
878
|
+
padding: 0.55rem 1.4rem;
|
|
879
|
+
pointer-events: none;
|
|
880
|
+
z-index: 25;
|
|
881
|
+
background: transparent;
|
|
882
|
+
}
|
|
883
|
+
footer.deck-chrome .right { display: flex; gap: 0.45rem; pointer-events: auto; }
|
|
884
|
+
footer.deck-chrome button {
|
|
885
|
+
background: transparent;
|
|
886
|
+
border: 1px solid var(--rule-soft);
|
|
887
|
+
color: var(--ink-2);
|
|
888
|
+
padding: 0.32rem 0.7rem;
|
|
889
|
+
border-radius: 0;
|
|
890
|
+
font-family: 'Fraunces', serif;
|
|
891
|
+
font-variation-settings: 'opsz' 9, 'wght' 600;
|
|
892
|
+
font-size: 0.62rem;
|
|
893
|
+
letter-spacing: 0.28em;
|
|
894
|
+
text-transform: uppercase;
|
|
895
|
+
cursor: pointer;
|
|
896
|
+
transition: color 0.2s, border-color 0.2s;
|
|
897
|
+
}
|
|
898
|
+
footer.deck-chrome button:hover {
|
|
899
|
+
color: var(--accent);
|
|
900
|
+
border-color: var(--accent);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* ---------- Help overlay ---------- */
|
|
904
|
+
.help-overlay {
|
|
905
|
+
position: fixed;
|
|
906
|
+
inset: 0;
|
|
907
|
+
background: rgba(26,24,22,0.78);
|
|
908
|
+
display: none;
|
|
909
|
+
align-items: center;
|
|
910
|
+
justify-content: center;
|
|
911
|
+
z-index: 100;
|
|
912
|
+
backdrop-filter: blur(2px);
|
|
913
|
+
}
|
|
914
|
+
[data-theme="dark"] .help-overlay { background: rgba(0,0,0,0.85); }
|
|
915
|
+
.help-overlay.show { display: flex; }
|
|
916
|
+
.help-overlay .panel {
|
|
917
|
+
background: var(--paper);
|
|
918
|
+
border: 2px solid var(--rule);
|
|
919
|
+
border-radius: 0;
|
|
920
|
+
padding: 2.2rem 2.6rem;
|
|
921
|
+
max-width: 520px;
|
|
922
|
+
width: 90vw;
|
|
923
|
+
position: relative;
|
|
924
|
+
}
|
|
925
|
+
.help-overlay .panel::before {
|
|
926
|
+
content: "Manual";
|
|
927
|
+
position: absolute;
|
|
928
|
+
top: -0.7rem;
|
|
929
|
+
left: 1.5rem;
|
|
930
|
+
background: var(--paper);
|
|
931
|
+
padding: 0 0.6rem;
|
|
932
|
+
font-family: 'Fraunces', serif;
|
|
933
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
934
|
+
font-size: 0.62rem;
|
|
935
|
+
letter-spacing: 0.32em;
|
|
936
|
+
text-transform: uppercase;
|
|
937
|
+
color: var(--accent);
|
|
938
|
+
}
|
|
939
|
+
.help-overlay h3 {
|
|
940
|
+
font-family: 'Fraunces', serif;
|
|
941
|
+
font-variation-settings: 'opsz' 96, 'wght' 700;
|
|
942
|
+
font-size: 1.4rem;
|
|
943
|
+
margin-bottom: 1.1rem;
|
|
944
|
+
color: var(--ink);
|
|
945
|
+
letter-spacing: -0.015em;
|
|
946
|
+
border-bottom: 1px solid var(--rule);
|
|
947
|
+
padding-bottom: 0.7rem;
|
|
948
|
+
}
|
|
949
|
+
.help-overlay table { width: 100%; }
|
|
950
|
+
.help-overlay td {
|
|
951
|
+
padding: 0.45rem 0.5rem;
|
|
952
|
+
font-family: 'Newsreader', serif;
|
|
953
|
+
font-size: 0.95rem;
|
|
954
|
+
color: var(--ink);
|
|
955
|
+
border-bottom: 1px dotted var(--rule-soft);
|
|
956
|
+
}
|
|
957
|
+
.help-overlay tr:last-child td { border-bottom: none; }
|
|
958
|
+
.help-overlay td:first-child {
|
|
959
|
+
font-family: 'JetBrains Mono', monospace;
|
|
960
|
+
color: var(--accent);
|
|
961
|
+
white-space: nowrap;
|
|
962
|
+
font-weight: 500;
|
|
963
|
+
width: 35%;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/* ---------- Notes panel ---------- */
|
|
967
|
+
.notes-panel {
|
|
968
|
+
position: fixed;
|
|
969
|
+
bottom: 0; left: 0; right: 0;
|
|
970
|
+
background: var(--paper);
|
|
971
|
+
border-top: 3px double var(--rule);
|
|
972
|
+
padding: 1.3rem 2rem 1.6rem;
|
|
973
|
+
max-height: 38vh;
|
|
974
|
+
overflow-y: auto;
|
|
975
|
+
display: none;
|
|
976
|
+
font-family: 'Newsreader', serif;
|
|
977
|
+
font-variation-settings: 'opsz' 22;
|
|
978
|
+
font-size: 0.95rem;
|
|
979
|
+
color: var(--ink);
|
|
980
|
+
z-index: 50;
|
|
981
|
+
box-shadow: 0 -3px 0 var(--paper);
|
|
982
|
+
}
|
|
983
|
+
.notes-panel.show { display: block; }
|
|
984
|
+
.notes-panel h4 {
|
|
985
|
+
font-family: 'Fraunces', serif;
|
|
986
|
+
font-variation-settings: 'opsz' 9, 'wght' 700;
|
|
987
|
+
font-size: 0.7rem;
|
|
988
|
+
letter-spacing: 0.32em;
|
|
989
|
+
text-transform: uppercase;
|
|
990
|
+
color: var(--accent);
|
|
991
|
+
margin-bottom: 0.7rem;
|
|
992
|
+
padding-bottom: 0.5rem;
|
|
993
|
+
border-bottom: 1px solid var(--rule);
|
|
994
|
+
}
|
|
995
|
+
.notes-panel p {
|
|
996
|
+
font-family: 'Newsreader', serif;
|
|
997
|
+
line-height: 1.55;
|
|
998
|
+
max-width: 90ch;
|
|
999
|
+
}
|
|
1000
|
+
.notes-panel p::first-letter {
|
|
1001
|
+
font-family: 'Fraunces', serif;
|
|
1002
|
+
font-variation-settings: 'opsz' 144, 'wght' 700;
|
|
1003
|
+
font-size: 2.4rem;
|
|
1004
|
+
float: left;
|
|
1005
|
+
line-height: 0.85;
|
|
1006
|
+
margin: 0.18rem 0.5rem 0 0;
|
|
1007
|
+
color: var(--accent);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/* ---------- Reduced motion ---------- */
|
|
1011
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1012
|
+
*, *::before, *::after {
|
|
1013
|
+
animation-duration: 0.01ms !important;
|
|
1014
|
+
transition-duration: 0.2s !important;
|
|
1015
|
+
}
|
|
1016
|
+
.slide.active > *,
|
|
1017
|
+
.kicker::before { animation: none !important; }
|
|
1018
|
+
body { transition: none !important; }
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/* ---------- Height-based responsive scaling ---------- */
|
|
1022
|
+
@media (max-height: 800px) {
|
|
1023
|
+
:root {
|
|
1024
|
+
--slide-pad-top: clamp(0.7rem, 2vh, 1.4rem);
|
|
1025
|
+
--slide-pad-bottom: clamp(2.2rem, 3vh, 3rem);
|
|
1026
|
+
}
|
|
1027
|
+
.slide h1 { font-size: clamp(2rem, 4.2vw, 3.4rem); margin-bottom: 0.9rem; }
|
|
1028
|
+
.slide h2 { font-size: clamp(1.35rem, 2.4vw, 2rem); margin-bottom: 0.7rem; }
|
|
1029
|
+
.kicker { margin-bottom: 0.8rem; font-size: 0.66rem; }
|
|
1030
|
+
.arch-flow { font-size: clamp(0.66rem, 0.85vw, 0.85rem); padding: 0.85rem 1.2rem; line-height: 1.45; }
|
|
1031
|
+
.slide pre { font-size: clamp(0.72rem, 0.9vw, 0.88rem); line-height: 1.5; padding: 0.95rem 1.2rem 0.85rem; }
|
|
1032
|
+
.kpi .num { font-size: clamp(1.4rem, 2.4vw, 2.2rem); }
|
|
1033
|
+
.big-text { font-size: clamp(1.55rem, 3vw, 2.7rem); }
|
|
1034
|
+
.stake { padding-top: 0.55rem; padding-bottom: 0.6rem; }
|
|
1035
|
+
.lesson { padding: 0.7rem 0 0.8rem; }
|
|
1036
|
+
.attempt { padding: 0.7rem 0 0.8rem; }
|
|
1037
|
+
.timeline { gap: 0.5rem 1.2rem; }
|
|
1038
|
+
}
|
|
1039
|
+
@media (max-height: 700px) {
|
|
1040
|
+
:root {
|
|
1041
|
+
--slide-pad-top: 0.7rem;
|
|
1042
|
+
--slide-pad-bottom: 2.6rem;
|
|
1043
|
+
--slide-pad-x: clamp(1.2rem, 4vw, 3rem);
|
|
1044
|
+
}
|
|
1045
|
+
.slide h1 { font-size: clamp(1.7rem, 3.6vw, 2.8rem); margin-bottom: 0.6rem; }
|
|
1046
|
+
.slide h2 { font-size: clamp(1.15rem, 2vw, 1.7rem); margin-bottom: 0.55rem; }
|
|
1047
|
+
.arch-flow { font-size: clamp(0.6rem, 0.78vw, 0.78rem); padding: 0.65rem 1rem; line-height: 1.4; }
|
|
1048
|
+
.slide pre { font-size: clamp(0.66rem, 0.82vw, 0.8rem); line-height: 1.45; padding: 0.7rem 1rem 0.65rem; }
|
|
1049
|
+
.kpi { padding: 0.7rem 1rem 0.6rem; }
|
|
1050
|
+
.kpi .num { font-size: clamp(1.2rem, 2vw, 1.85rem); margin-bottom: 0.25rem; }
|
|
1051
|
+
.kpi .label { font-size: 0.74rem; }
|
|
1052
|
+
.big-text { font-size: clamp(1.3rem, 2.5vw, 2.1rem); }
|
|
1053
|
+
.lesson .head { font-size: clamp(0.95rem, 1.4vw, 1.2rem); }
|
|
1054
|
+
.lesson .body { font-size: clamp(0.82rem, 1vw, 0.94rem); }
|
|
1055
|
+
.stakeholders { font-size: 0.94em; }
|
|
1056
|
+
.stake .who { font-size: clamp(0.86rem, 1.1vw, 1rem); }
|
|
1057
|
+
.stake .claim { font-size: clamp(0.76rem, 0.92vw, 0.88rem); line-height: 1.35; }
|
|
1058
|
+
.timeline > div:nth-child(2n) { font-size: clamp(0.85rem, 1.1vw, 1rem); padding-bottom: 0.45rem; }
|
|
1059
|
+
.timeline { gap: 0.45rem 1rem; padding-top: 0.7rem; }
|
|
1060
|
+
.timeline .stage { font-size: clamp(1rem, 1.6vw, 1.4rem); min-width: 4rem; }
|
|
1061
|
+
.callout { font-size: clamp(0.85rem, 1.15vw, 1rem); margin: 0.6rem 0 0.3rem; padding: 0.5rem 0; }
|
|
1062
|
+
.callout::before { font-size: 1.4rem; left: -1.2rem; }
|
|
1063
|
+
.layers .h { font-size: 0.6rem; padding: 0.55rem 0.85rem; }
|
|
1064
|
+
.layers .c { padding: 0.55rem 0.85rem; font-size: clamp(0.82rem, 1.05vw, 0.96rem); }
|
|
1065
|
+
.layers .c.l-name { font-size: clamp(0.88rem, 1.15vw, 1.05rem); }
|
|
1066
|
+
.chaos-grid .box { padding: 0.6rem 0.85rem; font-size: clamp(0.7rem, 0.92vw, 0.85rem); }
|
|
1067
|
+
.attempt { grid-template-columns: 4rem 1fr 6rem; gap: 1rem; }
|
|
1068
|
+
.attempt .ver { font-size: clamp(1.4rem, 2.2vw, 1.9rem); }
|
|
1069
|
+
.attempt .desc { font-size: clamp(0.84rem, 1.1vw, 1rem); }
|
|
1070
|
+
.attempt .verdict { font-size: 0.6rem; padding: 0.4rem 0.6rem; }
|
|
1071
|
+
.lesson { grid-template-columns: 6rem 1fr; gap: 1rem; }
|
|
1072
|
+
.lesson .num { font-size: 0.6rem; padding-top: 0.15rem; }
|
|
1073
|
+
.lesson .num::after { width: 1.1rem; margin-top: 0.3rem; }
|
|
1074
|
+
.title-slide h1 { font-size: clamp(2.4rem, 6vw, 4.4rem); }
|
|
1075
|
+
.title-slide .deck { font-size: clamp(1rem, 1.7vw, 1.4rem); margin-bottom: 1.4rem; }
|
|
1076
|
+
.title-slide .frontpage-rule { margin-bottom: 1.6rem; padding: 0.35rem 0.2rem; font-size: 0.62rem; }
|
|
1077
|
+
}
|
|
1078
|
+
@media (max-height: 600px) {
|
|
1079
|
+
:root {
|
|
1080
|
+
--slide-pad-top: 0.5rem;
|
|
1081
|
+
--slide-pad-bottom: 2.2rem;
|
|
1082
|
+
}
|
|
1083
|
+
.slide h1 { font-size: clamp(1.5rem, 3.2vw, 2.4rem); }
|
|
1084
|
+
.slide h2 { font-size: clamp(1rem, 1.7vw, 1.4rem); }
|
|
1085
|
+
.arch-flow { font-size: clamp(0.55rem, 0.7vw, 0.7rem); line-height: 1.35; padding: 0.5rem 0.85rem; }
|
|
1086
|
+
.slide pre { font-size: clamp(0.6rem, 0.74vw, 0.74rem); line-height: 1.4; padding: 0.55rem 0.85rem; }
|
|
1087
|
+
.stakeholders { font-size: 0.86em; }
|
|
1088
|
+
.stake { padding: 0.45rem 1rem 0.45rem 0; }
|
|
1089
|
+
.stake:nth-child(2n) { padding: 0.45rem 0 0.45rem 1rem; }
|
|
1090
|
+
.stake .claim { font-size: 0.78rem; line-height: 1.3; }
|
|
1091
|
+
.big-text { font-size: clamp(1.15rem, 2.2vw, 1.8rem); }
|
|
1092
|
+
.timeline { padding-top: 0.5rem; }
|
|
1093
|
+
.timeline > div:nth-child(2n) { font-size: clamp(0.78rem, 0.95vw, 0.9rem); padding-bottom: 0.35rem; }
|
|
1094
|
+
.lesson { padding: 0.55rem 0 0.65rem; }
|
|
1095
|
+
.lesson .head { font-size: clamp(0.88rem, 1.2vw, 1.05rem); margin-bottom: 0.2rem; }
|
|
1096
|
+
.lesson .body { font-size: clamp(0.78rem, 0.95vw, 0.88rem); line-height: 1.35; }
|
|
1097
|
+
.closing-page .signoff { font-size: 0.6rem; }
|
|
1098
|
+
footer.deck-chrome button { font-size: 0.58rem; padding: 0.25rem 0.55rem; }
|
|
1099
|
+
}
|
|
1100
|
+
@media (max-width: 720px) {
|
|
1101
|
+
:root { --slide-pad-x: 1.2rem; }
|
|
1102
|
+
.stakeholders, .chaos-grid, .two-col, .layers { grid-template-columns: 1fr; }
|
|
1103
|
+
.stake:nth-child(2n) { padding-left: 0 !important; }
|
|
1104
|
+
.stake:nth-child(2n) .who, .stake:nth-child(2n) .claim,
|
|
1105
|
+
.stake:nth-child(odd) .who, .stake:nth-child(odd) .claim { padding-left: 0 !important; padding-right: 1.5rem !important; }
|
|
1106
|
+
.stake::before { right: 0 !important; left: auto !important; }
|
|
1107
|
+
.lesson { grid-template-columns: 1fr; gap: 0.4rem; }
|
|
1108
|
+
.attempt { grid-template-columns: 4rem 1fr; }
|
|
1109
|
+
.attempt .verdict { grid-column: 1 / -1; justify-self: start; margin-top: 0.4rem; }
|
|
1110
|
+
.two-col::before { display: none; }
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/* ---------- Print mode (PDF export) ---------- */
|
|
1114
|
+
@media print {
|
|
1115
|
+
body {
|
|
1116
|
+
overflow: visible;
|
|
1117
|
+
height: auto;
|
|
1118
|
+
font-size: 13px;
|
|
1119
|
+
background: white;
|
|
1120
|
+
color: black;
|
|
1121
|
+
}
|
|
1122
|
+
body::before { display: none; }
|
|
1123
|
+
.masthead, footer.deck-chrome, .help-overlay, .notes-panel { display: none !important; }
|
|
1124
|
+
#deck { height: auto; position: static; padding-top: 0; }
|
|
1125
|
+
.slide {
|
|
1126
|
+
position: relative;
|
|
1127
|
+
page-break-after: always;
|
|
1128
|
+
height: auto;
|
|
1129
|
+
min-height: 96vh;
|
|
1130
|
+
top: 0;
|
|
1131
|
+
opacity: 1 !important;
|
|
1132
|
+
pointer-events: auto !important;
|
|
1133
|
+
padding: 5vh 7vw;
|
|
1134
|
+
animation: none !important;
|
|
1135
|
+
}
|
|
1136
|
+
.slide.active > * { animation: none !important; }
|
|
1137
|
+
.slide pre, .arch-flow { background: #f5f1e8; }
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/* ---------- Responsive guard ---------- */
|
|
1141
|
+
|
|
1142
|
+
</style>
|
|
1143
|
+
</head>
|
|
1144
|
+
<body data-theme="light">
|
|
1145
|
+
|
|
1146
|
+
<main id="deck">
|
|
1147
|
+
|
|
1148
|
+
<!-- 1. TITLE -->
|
|
1149
|
+
<section class="slide title-slide active" data-notes="Good morning, everyone! I'm so glad to be here talking about data access control in GraphQL. My name's Minghe — I work on the GraphQL Federation platform at Booking.com. Today I'll show you how we used a single Federation directive — @policy — to solve a pretty complex data access governance problem across our org. The punchline is the architecture: decentralized authorship, centralized enforcement. Every team writes their own rules; enforcement happens in one place. But honestly, the directive itself wasn't the hard part. The hard part was the coordination. Let me show you what I mean — starting with one field.">
|
|
1150
|
+
<h1>Coordinated<br>Access Control<br>with <code>@policy</code></h1>
|
|
1151
|
+
<div class="deck">Decentralized authorship, centralized enforcement — a coordination pattern for access control at scale.</div>
|
|
1152
|
+
<div class="byline">
|
|
1153
|
+
<span class="name">Minghe Huang</span>
|
|
1154
|
+
<span class="meta">GraphQL Federation Platform · Booking.com</span>
|
|
1155
|
+
</div>
|
|
1156
|
+
</section>
|
|
1157
|
+
|
|
1158
|
+
<!-- 2. THE FIELD -->
|
|
1159
|
+
<section class="slide" data-notes="Let's start with one innocent-looking field — User.email. Now I want to ask: who has a claim on what happens when someone reads User.email? If you've worked in a regulated industry — travel, fintech, healthcare, banking — you probably already know the answer is more than one. There are many. And each one has a legitimate claim, backed by policy, regulation, or sometimes just production reality. Let me walk you through who they are.">
|
|
1160
|
+
<div class="kicker">The Field <span class="folio">¶ 02</span></div>
|
|
1161
|
+
<h2>One field.<br>Innocent-looking.</h2>
|
|
1162
|
+
<pre><code>type User {
|
|
1163
|
+
id: ID!
|
|
1164
|
+
email: String
|
|
1165
|
+
}</code></pre>
|
|
1166
|
+
<p style="margin-top: 1.4rem; font-family: 'Newsreader', serif; font-style: italic; font-size: clamp(1.2rem, 1.9vw, 1.55rem); color: var(--ink); max-width: 60ch; font-variation-settings: 'opsz' 36;">Who in the company has a claim on what happens when this field is read?</p>
|
|
1167
|
+
</section>
|
|
1168
|
+
|
|
1169
|
+
<!-- 3. SIX STAKEHOLDERS -->
|
|
1170
|
+
<section class="slide" data-notes="When I was working on this problem, I tried to sort out who actually has a stake. At Booking.com, six groups of teams have a real claim on this field. And each group isn't just one team — it's many teams, all with concerns about data access at different levels and from different perspectives. Some are obvious, some are less obvious. The obvious ones. Legal and Privacy are the source of truth about data protection at the company. They define how we handle data under GDPR, PIPL, DMA, and the equivalent regulations in every market we operate in. Security, Identity, and Sessions define what authorized and authenticated mean — they're concerned with the right caller accessing the right data. And the domain data teams behind every subgraph care about one thing: I just want to serve the data for my product features. You guys deal with access control. Then the less obvious ones. The Traffic Gateway sits at the edge — every external request hits it first. Its job is enrichment: adding domain-specific information so requests come into our internal systems with a consistent structure. It also handles basic traffic management — bot detection, blocking malicious requests at the edge. Data Governance maintains the metadata graph for every dataset in the company. Their core principle: every data consumption has to come with a documented business purpose. They also handle data classification and lineage. And the GraphQL Federation Platform — that's my team — we focus on federation: decomposing client queries, distributing them to domain subgraphs, and aggregating the responses back to the client. Six lanes. All of them legitimately care about data access — that's just a fact. The real question is how to have a coordinated solution that natively connects these concerns and solves them as a whole. Today, there's no single place where these teams compose their access rules together. And no single place where those rules get evaluated when data is actually read. That's the gap. And the gap creates real challenges.">
|
|
1171
|
+
<div class="kicker">The Stakeholders <span class="folio">¶ 03</span></div>
|
|
1172
|
+
<h2>Many departments have a claim. Every claim is real.</h2>
|
|
1173
|
+
<div class="stakeholders">
|
|
1174
|
+
<div class="stake-head">Obvious</div>
|
|
1175
|
+
<div class="stake-head right-col">Less obvious</div>
|
|
1176
|
+
<div class="stake">
|
|
1177
|
+
<div class="who">Legal & Privacy</div>
|
|
1178
|
+
<div class="claim">Defines lawful basis under GDPR, PIPL, DMA, and similar regulations; runs the consent platform — the single record of every user choice. Concern: <strong>data must honor consent.</strong></div>
|
|
1179
|
+
</div>
|
|
1180
|
+
<div class="stake">
|
|
1181
|
+
<div class="who">Traffic Gateway</div>
|
|
1182
|
+
<div class="claim">First hop for every external request. Mints IAM context, enriches with session and device signals, runs bot detection and throttling. Concern: <strong>block abuse before the field is read.</strong></div>
|
|
1183
|
+
</div>
|
|
1184
|
+
<div class="stake">
|
|
1185
|
+
<div class="who">Security · Identity · Session</div>
|
|
1186
|
+
<div class="claim">Defines what "authorized" means (least privilege, ABAC). Issues every access token that proves it — IAM, service-to-service — with caller identity, scopes, and assurance level. Concern: <strong>the right caller, the right data.</strong></div>
|
|
1187
|
+
</div>
|
|
1188
|
+
<div class="stake">
|
|
1189
|
+
<div class="who">Data Governance</div>
|
|
1190
|
+
<div class="claim">Maintains the metadata graph for every dataset — classification, lineage, business purpose. Knows how data is born, transformed, and consumed. Concern: <strong>every flow has a documented purpose.</strong></div>
|
|
1191
|
+
</div>
|
|
1192
|
+
<div class="stake">
|
|
1193
|
+
<div class="who">Domain Data Teams × hundreds</div>
|
|
1194
|
+
<div class="claim">The teams behind every subgraph — user, partner, payment, reservation, content. They serve the actual data. Concern: <strong>just let me serve my domain data.</strong></div>
|
|
1195
|
+
</div>
|
|
1196
|
+
<div class="stake">
|
|
1197
|
+
<div class="who">GraphQL Federation Platform</div>
|
|
1198
|
+
<div class="claim">The single GraphQL endpoint over hundreds of domain subgraphs. Owns the supergraph router, the schema pipeline, the policy plugin. Concern: <strong>make every team's rules enforceable in one place.</strong></div>
|
|
1199
|
+
</div>
|
|
1200
|
+
</div>
|
|
1201
|
+
</section>
|
|
1202
|
+
|
|
1203
|
+
<!-- 4. COORDINATION COST -->
|
|
1204
|
+
<section class="slide" data-notes="Here's what those challenges looked like. Without a single place to compose rules or evaluate them, onboarding one new sensitive field could take weeks. Teams had to file tickets, ask stakeholders to review the sensitive data, and schedule cross-team meetings to discuss how the audit trails and audit logs should be set up. After all that, the domain teams still had to write the security code themselves — ad-hoc defensive code at the subgraph level. Totally inconsistent across subgraphs. Duplication everywhere. N+ separate audit trails — one per enforcement system, multiplied every time a regulator asked a question. Zero places where anyone could see all the enforcement applied to a single field. And constant drift — every team's changes just made things worse. Now, if we'd treated this as a security problem, we'd have written more security code. We treated it as a coordination problem. And we asked one question: if this is coordination, where should it live?">
|
|
1205
|
+
<div class="kicker">The Cost <span class="folio">¶ 04</span></div>
|
|
1206
|
+
<h2>The coordination cost, by the numbers.</h2>
|
|
1207
|
+
<div class="kpi-grid">
|
|
1208
|
+
<div class="kpi"><div class="num">Weeks</div><div class="label">to onboard a new sensitive field</div></div>
|
|
1209
|
+
<div class="kpi"><div class="num">N+</div><div class="label">audit trails per regulator question</div></div>
|
|
1210
|
+
<div class="kpi"><div class="num">0</div><div class="label">places to see all enforcement on one field</div></div>
|
|
1211
|
+
<div class="kpi"><div class="num">N+</div><div class="label">drift — every change made it worse</div></div>
|
|
1212
|
+
</div>
|
|
1213
|
+
<p class="muted">If we'd treated this as a security problem, we'd have written more security code. We treated it as a <strong>coordination</strong> problem. That changed everything.</p>
|
|
1214
|
+
</section>
|
|
1215
|
+
|
|
1216
|
+
<!-- 5. THE INSIGHT -->
|
|
1217
|
+
<section class="slide" data-notes="The schema. The schema is already a contract — between data consumers and data providers. So we made it the contract for access control too. Three things make this pattern work. The schema becomes the coordination contract — that's where every team declares which policies apply to which fields. The policy engine becomes the unified interface — where each team authors their own access-control rules independently. And the router becomes the unified enforcer — one place where every policy gets evaluated, on every request. The good thing is we didn't reinvent anything. We already had an internal policy engine at Booking. We leveraged the federation-native @policy directive. And my team built the bridge — collecting request context in the router, translating it into what the engine expects, and enforcing the verdict. Decentralized authorship. Centralized enforcement. Let me show you what it looks like in the schema itself.">
|
|
1218
|
+
<div class="kicker">The Insight <span class="folio">¶ 05</span></div>
|
|
1219
|
+
<span class="insight-mark">¶</span>
|
|
1220
|
+
<div class="big-text">
|
|
1221
|
+
<span class="accent">Schema</span> = the coordination contract.<br>
|
|
1222
|
+
<span class="accent">Policy engine</span> = where teams author their concerns independently.<br>
|
|
1223
|
+
<span class="accent">Router</span> = the unified enforcer.
|
|
1224
|
+
</div>
|
|
1225
|
+
<div class="arch-diagram">
|
|
1226
|
+
<span class="label">teams</span> <span class="label">teams</span>
|
|
1227
|
+
<span class="hop">│</span> <span class="hop">│</span>
|
|
1228
|
+
writes annotates
|
|
1229
|
+
<span class="hop">↓</span> <span class="hop">↓</span>
|
|
1230
|
+
<span class="step">POLICY ENGINE</span> <span class="step">SCHEMA</span>
|
|
1231
|
+
<span class="label">decentralized</span> <span class="accent">@policy()</span> <span class="label">on fields</span>
|
|
1232
|
+
<span class="hop">│</span> <span class="hop">│</span>
|
|
1233
|
+
<span class="hop">└──────────</span> consulted by <span class="hop">──────────────┘</span>
|
|
1234
|
+
<span class="hop">│</span>
|
|
1235
|
+
<span class="hop">↓</span>
|
|
1236
|
+
<span class="step">ROUTER</span> <span class="hop">←</span> <span class="label">client query</span>
|
|
1237
|
+
<span class="label">centralized enforcement</span>
|
|
1238
|
+
<span class="hop">│</span>
|
|
1239
|
+
<span class="hop">↓</span>
|
|
1240
|
+
<span class="ok">allow</span> / <span class="bad">deny</span>
|
|
1241
|
+
<span class="label">per field</span>
|
|
1242
|
+
</div>
|
|
1243
|
+
</section>
|
|
1244
|
+
|
|
1245
|
+
<!-- 6. @POLICY -->
|
|
1246
|
+
<section class="slide" data-notes="On screen are five real production policies we use at Booking.com today. They fall into three groups. Domain teams design feature-specific rules. User.totalNights — only services on an allowlist can access it. That's a service allowlist. Hotel.geniusBenefit — only users at Genius Level 3 see it. That's a user-tier gate. Both come from the domain team, driven by their product requirements. Privacy and Legal contribute cross-cutting rules. Look at User.email — there are two policies attached. Privacy's marketing-consent requires the user has given consent before email is processed. OR the caller is the owner or an authorized CS agent. Either passing is enough. That's how a cross-cutting rule and a domain rule combine. And Property.euOnlyDetails is geo-restricted to EU residents — also from Privacy. Both apply across many fields and many domains. And then there's cross-domain reuse. Insurance.details — the Insurance team doesn't write its own access rule. They reuse the Order team's access-check, because access to insurance fundamentally depends on access to the underlying order. That same rule applies to dozens of order-dependent fields across the federation. Five patterns, three groups. Domain teams author. Privacy and Legal contribute. Other domains reuse. And these can be composed with AND/OR semantics on any field. Now let me show you how it actually runs at the router.">
|
|
1247
|
+
<div class="kicker">The Directive <span class="folio">¶ 06</span></div>
|
|
1248
|
+
<h2><code>@policy</code> — your domain rules, plus what crosses them.</h2>
|
|
1249
|
+
<pre><code>type User {
|
|
1250
|
+
totalNights: Int
|
|
1251
|
+
@policy(policies: [["user/loyalty-service-only"]]) <span style="color: var(--accent);"># service allowlist</span>
|
|
1252
|
+
|
|
1253
|
+
email: String
|
|
1254
|
+
@policy(policies: [
|
|
1255
|
+
["privacy/marketing-consent"], <span style="color: var(--accent);"># consent (cross-cutting)</span>
|
|
1256
|
+
["user/owner-or-cs-agent"] <span style="color: var(--accent);"># OR — caller is owner / CS agent</span>
|
|
1257
|
+
])
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
type Hotel {
|
|
1261
|
+
geniusBenefit: Money
|
|
1262
|
+
@policy(policies: [["user/genius-level-3"]]) <span style="color: var(--accent);"># user-tier gate</span>
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
type Property {
|
|
1266
|
+
euOnlyDetails: String
|
|
1267
|
+
@policy(policies: [["privacy/eu-residents-only"]]) <span style="color: var(--accent);"># geo restriction</span>
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
type Insurance {
|
|
1271
|
+
details: String
|
|
1272
|
+
@policy(policies: [["order/access-check"]]) <span style="color: var(--accent);"># reused from Order team</span>
|
|
1273
|
+
}</code></pre>
|
|
1274
|
+
<div class="callout">
|
|
1275
|
+
<strong>Domain teams author the rules for their own data. Privacy and Legal layer in cross-cutting concerns. Other domains reuse</strong> — Insurance access depends on order access, so it reuses <code>order/access-check</code>. The Federation Platform plumbs the runtime.
|
|
1276
|
+
</div>
|
|
1277
|
+
</section>
|
|
1278
|
+
|
|
1279
|
+
<!-- 7. ARCHITECTURE & TRACE -->
|
|
1280
|
+
<section class="slide" data-notes="Let me walk you through one real request, end to end. A support tool queries User.email. The request comes in with everything it needs — declared purpose customer-support, an IAM token with the right scope, and Traffic Gateway context already attached: bot classification, locale, device. Step one: the router pulls all of that out. Step two — and this is the part that matters — during query planning, for every field with a @policy directive, the router calls the central policy engine in parallel, on every rule attached. So Privacy's marketing-consent rule comes back as fail — the user opted out of marketing. The User team's owner-or-cs-agent rule comes back as pass — the caller is a verified CS agent. The schema says OR, so either one passing is enough. The field comes back. Single trace ID logged for the regulator — that's critical for legal and compliance. Step three: the verdict shapes the plan. Allowed fields stay in the plan and get queried as normal. Denied fields are stripped — the subgraph never sees the request. So no unauthorized sensitive data ever leaves the domain subgraph. The client gets null plus an unauthorized error. Now look at what just happened. Three different teams contributed to this single decision, with no dependencies between them. Privacy and Legal authored the consent rule. The User team authored owner-or-cs-agent. And my team — the Federation Platform — built the bridge that calls the engine and enforces the verdict in the router. Each owns their own lane. The schema is the contract that ties them together. Everything in one place, in harmony. Let me zoom out and show you how this generalizes.">
|
|
1281
|
+
<div class="kicker">Architecture <span class="folio">¶ 07</span></div>
|
|
1282
|
+
<h2>One request, end to end.</h2>
|
|
1283
|
+
<div class="arch-flow">
|
|
1284
|
+
<span class="hop">client →</span> <span class="step">federation router</span>
|
|
1285
|
+
|
|
1286
|
+
<span class="label">request:</span> <span class="step">query { user(id: "...") { email } }</span>
|
|
1287
|
+
<span class="label">purpose:</span> customer-support
|
|
1288
|
+
<span class="label">caller:</span> support-tools (S2S) · AAL2 · scope=user.read
|
|
1289
|
+
|
|
1290
|
+
<span class="hop">├─</span> <span class="label">extract context</span> (IAM, S2S, scopes, purpose, traffic-gw signals)
|
|
1291
|
+
<span class="hop">│</span>
|
|
1292
|
+
<span class="hop">├─</span> <span class="label">during query planning, for each @policy field:</span>
|
|
1293
|
+
<span class="hop">│</span> <span class="step">call central policy engine</span> in parallel
|
|
1294
|
+
<span class="hop">│</span> <span class="hop">↓</span>
|
|
1295
|
+
<span class="hop">│</span> <span class="pol">privacy/marketing-consent</span> <span class="bad">×</span> user opted out of marketing
|
|
1296
|
+
<span class="hop">│</span> <span class="pol">user/owner-or-cs-agent</span> <span class="ok">✓</span> caller is verified CS agent
|
|
1297
|
+
<span class="hop">│</span>
|
|
1298
|
+
<span class="hop">├─</span> <span class="step">combine (OR / AND per schema)</span> → <span class="step">DECISION</span> <span class="ok">ALLOW</span> → <span class="step">single audit log</span>
|
|
1299
|
+
<span class="hop">│</span>
|
|
1300
|
+
<span class="hop">├─</span> <span class="ok">✓ allowed</span> → field stays in plan → subgraph queried
|
|
1301
|
+
<span class="hop">└─</span> <span class="bad">× denied</span> → field stripped from plan → null + <code>"unauthorized"</code>
|
|
1302
|
+
<span class="label">(subgraph never sees the request for that field)</span>
|
|
1303
|
+
</div>
|
|
1304
|
+
<p class="muted">Three teams contributed: Privacy's cross-cutting rule, the User team's domain rule, and the Federation Platform's runtime. <strong>None coordinated. The schema held them together.</strong> Denied data never leaves the router.</p>
|
|
1305
|
+
</section>
|
|
1306
|
+
|
|
1307
|
+
<!-- 8. COORDINATION PATTERN -->
|
|
1308
|
+
<section class="slide" data-notes="Basically, this pattern has three layers. First — schema annotation. When fields are designed, the subgraph team puts the proper @policy directive on them. They pick which policies apply. Second — policy logic. Authored on the centralized policy platform. Every team writes their own rules based on their own requirements. When rules change — because compliance changes, or product requirements change — they just update the policy in the engine. Privacy can change consent rules without telling Identity. Each team in their own lane, on their own cadence. Third — enforcement runtime. That's my team, the Federation Platform. We don't own the policy engine; we own the bridge. We collect the request context in the router, transform it into what the engine expects, and enforce the verdict. Three layers, three owners, three cadences. No cross-team coordination needed after the schema annotation lands. And without all those meetings, the time to onboard a sensitive field collapses.">
|
|
1309
|
+
<div class="kicker">The Pattern <span class="folio">¶ 08</span></div>
|
|
1310
|
+
<h2>Three layers. Three cadences. No cross-team meetings.</h2>
|
|
1311
|
+
<div class="layers">
|
|
1312
|
+
<div class="h">Layer</div>
|
|
1313
|
+
<div class="h">Owner</div>
|
|
1314
|
+
<div class="h">Cadence</div>
|
|
1315
|
+
|
|
1316
|
+
<div class="c l-name">Schema annotation</div>
|
|
1317
|
+
<div class="c l-owner">Subgraph team</div>
|
|
1318
|
+
<div class="c l-cadence">When fields are designed</div>
|
|
1319
|
+
|
|
1320
|
+
<div class="c l-name">Policy logic</div>
|
|
1321
|
+
<div class="c l-owner">Authoring teams (Domain · Privacy · Identity · …)</div>
|
|
1322
|
+
<div class="c l-cadence">When their rules change</div>
|
|
1323
|
+
|
|
1324
|
+
<div class="c l-name">Enforcement runtime</div>
|
|
1325
|
+
<div class="c l-owner">Platform team</div>
|
|
1326
|
+
<div class="c l-cadence">When the platform evolves</div>
|
|
1327
|
+
</div>
|
|
1328
|
+
<p>Three layers. Three cadences. <strong>No mandatory cross-team coordination after the schema annotation lands.</strong></p>
|
|
1329
|
+
</section>
|
|
1330
|
+
|
|
1331
|
+
<!-- 9. ONBOARDING BEFORE/AFTER -->
|
|
1332
|
+
<section class="slide" data-notes="Here's what actually changed when we shipped this. Before: onboarding one new sensitive field took weeks — tickets to every stakeholder, cross-team meetings, enforcement code duplicated across systems, arguments about audit trails. After: the subgraph team just adds the @policy directive with the right policy IDs. The policies already exist — each team authored them once. The audit trail comes free. Single trace ID across every decision. Hours instead of weeks. Now, that's one team, one field. The real question is: what happens when you do this across hundreds of teams?">
|
|
1333
|
+
<div class="kicker">Before / After <span class="folio">¶ 09</span></div>
|
|
1334
|
+
<h2>Onboarding a new sensitive field.</h2>
|
|
1335
|
+
<div class="two-col">
|
|
1336
|
+
<div class="col">
|
|
1337
|
+
<div class="col-head before">Before</div>
|
|
1338
|
+
<ul>
|
|
1339
|
+
<li>File tickets with every stakeholder team</li>
|
|
1340
|
+
<li>Schedule cross-team meetings</li>
|
|
1341
|
+
<li>Write enforcement code that duplicates logic from other systems</li>
|
|
1342
|
+
<li>Argue about audit trails, then consolidate per regulator question</li>
|
|
1343
|
+
</ul>
|
|
1344
|
+
<div class="kpi" style="margin-top: 1rem; padding: 1rem 0; border-top: 2px solid var(--accent); border-right: none;">
|
|
1345
|
+
<div class="num" style="color: var(--accent);">Weeks</div>
|
|
1346
|
+
<div class="label">typical</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
<div class="col">
|
|
1350
|
+
<div class="col-head after">After</div>
|
|
1351
|
+
<ul>
|
|
1352
|
+
<li>Add <code>@policy</code> with the relevant policy IDs</li>
|
|
1353
|
+
<li>Policies already exist (each team authored once)</li>
|
|
1354
|
+
<li>Audit trail comes for free</li>
|
|
1355
|
+
<li>Single trace ID across all decisions</li>
|
|
1356
|
+
<li>No duplicated logic across systems.</li>
|
|
1357
|
+
</ul>
|
|
1358
|
+
<div class="kpi" style="margin-top: 1rem; padding: 1rem 0; border-top: 2px solid var(--good); border-right: none;">
|
|
1359
|
+
<div class="num" style="color: var(--good);">Hours</div>
|
|
1360
|
+
<div class="label">in practice</div>
|
|
1361
|
+
</div>
|
|
1362
|
+
</div>
|
|
1363
|
+
</div>
|
|
1364
|
+
</section>
|
|
1365
|
+
|
|
1366
|
+
<!-- 10. ADOPTION STORY -->
|
|
1367
|
+
<section class="slide" data-notes="Here's the thing. In a big organization, building the feature is only maybe 20% of the whole story. So we built the solution. Shipped it. Tested it. Pushed it to prod. Then we did all the usual outreach — internal training, internal documentation, brown-bags, Slack posts, the works. A few months in? About 10% of teams had picked it up. Now, that 10% wasn't random. The ones who adopted were mostly the new subgraphs — teams adding fresh fields. For them, @policy saves a lot of effort: they get coordinated access control plus the audit trail for free. They want it. But for the legacy subgraphs already in production? They have no motivation to migrate. Adopting doesn't save them work — it creates new work. New schema reviews, new policy decisions, new MRs to ship. Two hundred-plus data provider teams across the federation. Ten percent means about twenty onboarded, almost two hundred still exposing fields without coordinated access control. Of course, we could escalate — go to the Security and Privacy teams, have them file vulnerability tickets against every legacy subgraph, and force the schema reviews that way. But that's not smart. It creates an adversarial loop and shifts the burden to other authority teams. So we found a smarter way.">
|
|
1368
|
+
<div class="kicker">The Adoption Gap <span class="folio">¶ 10</span></div>
|
|
1369
|
+
<div class="big-text" style="font-size: clamp(1.5rem, 2.8vw, 2.2rem); margin-bottom: 1.4rem;">
|
|
1370
|
+
Build it, announce it.<br>
|
|
1371
|
+
<span class="accent">10%</span> will come.
|
|
1372
|
+
</div>
|
|
1373
|
+
<div class="kpi-grid">
|
|
1374
|
+
<div class="kpi"><div class="num">~10%</div><div class="label">organic adoption after several months</div></div>
|
|
1375
|
+
<div class="kpi"><div class="num">200+</div><div class="label">data provider teams across the federation</div></div>
|
|
1376
|
+
<div class="kpi"><div class="num">~9 mos</div><div class="label">to manually onboard the rest at 5 teams/week</div></div>
|
|
1377
|
+
</div>
|
|
1378
|
+
<div class="callout">
|
|
1379
|
+
<strong>Organic adoption doesn't scale to hundreds of teams.</strong> File tickets, walk each team through migration, repeat — the math doesn't work. We needed an approach that didn't depend on every team remembering.
|
|
1380
|
+
</div>
|
|
1381
|
+
</section>
|
|
1382
|
+
|
|
1383
|
+
<!-- 11. LLM DETECTION -->
|
|
1384
|
+
<section class="slide" data-notes="So we built a smarter way. A GraphQL security assessment agent that runs 24/7. Five steps. It detects — scans every subgraph schema and the resolver code behind each field, looking for security and compliance gaps against access-control rules from the authority teams. It proposes — when it finds a gap, it opens a merge request straight to the subgraph repo, with the recommended @policy annotation already filled in. And critically, with a comprehensive explanation of why this matters and how the proposed change works. It pings — asks the right team for review. Not a ticket, not a meeting. A real PR, ready to merge, with all the context the team needs to decide. It monitors — actively watches the centralized observability for every MR through to resolution and escalates the ones that go stale. It refreshes — keeps the centralized gap-tracking dashboard fresh, so leadership and the Security team can actually watch adoption move in real time. The numbers today: more than a thousand sensitive fields surfaced. About a hundred teams have acted on agent-proposed MRs. None of that would've happened through manual outreach. The agent does the part that just doesn't scale. Now — here's what we learned from all of it.">
|
|
1385
|
+
<div class="kicker">Closing the Loop <span class="folio">¶ 11</span></div>
|
|
1386
|
+
<h2>An agent finds the gaps. And proposes the fix.</h2>
|
|
1387
|
+
<p>A long-running agent that drives adoption directly — no tickets, no meetings.</p>
|
|
1388
|
+
<ul>
|
|
1389
|
+
<li><strong>Detect.</strong> Scan every subgraph schema and the resolver code behind it, against access-control requirements from authority teams.</li>
|
|
1390
|
+
<li><strong>Propose.</strong> Open a merge request with the recommended <code>@policy</code> already filled in.</li>
|
|
1391
|
+
<li><strong>Ping.</strong> Request review from the right team. A real PR, ready to merge.</li>
|
|
1392
|
+
<li><strong>Monitor.</strong> Watch every MR through to resolution. Escalate the stale ones.</li>
|
|
1393
|
+
<li><strong>Refresh.</strong> Update a centralized gap-tracking dashboard so leadership and Security can see adoption move.</li>
|
|
1394
|
+
</ul>
|
|
1395
|
+
<div class="kpi-grid" style="margin-top: 0.9rem;">
|
|
1396
|
+
<div class="kpi"><div class="num">1,000+</div><div class="label">sensitive fields surfaced across the federation</div></div>
|
|
1397
|
+
<div class="kpi"><div class="num">~100</div><div class="label">teams acted on agent-proposed MRs</div></div>
|
|
1398
|
+
</div>
|
|
1399
|
+
</section>
|
|
1400
|
+
|
|
1401
|
+
<!-- 12. THREE LESSONS -->
|
|
1402
|
+
<section class="slide" data-notes="Looking back, there are four lessons that stuck with me. And none of them are specific to federation. One. When more than two teams have legitimate claims on a piece of data, you don't have a security problem — you have a coordination problem. Solving it as security just makes it worse. Two. The schema is the natural coordination contract. Especially when the stakeholders come from very different backgrounds — engineering, legal, privacy, governance. The schema is the best shared language to make sure everyone is on the same page. Three. Single enforcement plus bounded authorship plus free reuse equals scale. One place decisions happen. Each team writes only the rules for their own data. Anyone can reuse anyone else's. Four. Adoption at scale doesn't come from outreach. You can't file a hundred tickets and expect a hundred teams to act. Build the agent that proposes the fix as a merge request — and the team just has to review. So — here's where it landed for us.">
|
|
1403
|
+
<div class="kicker">The Argument <span class="folio">¶ 12</span></div>
|
|
1404
|
+
<h2>Four lessons that generalize beyond GraphQL.</h2>
|
|
1405
|
+
<div class="lessons">
|
|
1406
|
+
<div class="lesson">
|
|
1407
|
+
<div class="num">Lesson i.</div>
|
|
1408
|
+
<div>
|
|
1409
|
+
<div class="head">Multi-stakeholder access control is a coordination problem — not a security one.</div>
|
|
1410
|
+
<div class="body">Solving it as security — more checks in more places — makes it worse. Treat it as coordination.</div>
|
|
1411
|
+
</div>
|
|
1412
|
+
</div>
|
|
1413
|
+
<div class="lesson">
|
|
1414
|
+
<div class="num">Lesson ii.</div>
|
|
1415
|
+
<div>
|
|
1416
|
+
<div class="head">The schema is the natural coordination contract.</div>
|
|
1417
|
+
<div class="body">Whatever your platform's interface is — GraphQL schema, gRPC proto, OpenAPI, event schema — that's where the contract lives.</div>
|
|
1418
|
+
</div>
|
|
1419
|
+
</div>
|
|
1420
|
+
<div class="lesson">
|
|
1421
|
+
<div class="num">Lesson iii.</div>
|
|
1422
|
+
<div>
|
|
1423
|
+
<div class="head">Single enforcement + bounded authorship + free reuse = scale.</div>
|
|
1424
|
+
<div class="body">One place decisions <em>happen</em>. Each team authors only their <em>own</em> data's rules. Anyone can reuse anyone else's rules.</div>
|
|
1425
|
+
</div>
|
|
1426
|
+
</div>
|
|
1427
|
+
<div class="lesson">
|
|
1428
|
+
<div class="num">Lesson iv.</div>
|
|
1429
|
+
<div>
|
|
1430
|
+
<div class="head">Adoption at scale needs automation — not outreach.</div>
|
|
1431
|
+
<div class="body">You can't ask hundreds of teams to remember. Build the agent that proposes the fix <em>as a merge request</em> — teams only have to review.</div>
|
|
1432
|
+
</div>
|
|
1433
|
+
</div>
|
|
1434
|
+
</div>
|
|
1435
|
+
</section>
|
|
1436
|
+
|
|
1437
|
+
<!-- 13. WHERE WE LANDED -->
|
|
1438
|
+
<section class="slide" data-notes="The numbers, briefly. More than a thousand access-control gaps surfaced and proposed as merge requests. About a hundred teams already adopted, more MRs in flight. Per-field onboarding 5× faster — what used to be weeks of cross-team coordination is now an annotation review on a pre-filled MR. And the regulator's favourite number — N+ audit trails consolidated down to one. One trace ID per query. Every decision logged together. Compliance, Privacy, Data Governance — they're all happy. So — here's the one thing I want you to take with you.">
|
|
1439
|
+
<div class="kicker">The Ledger <span class="folio">¶ 13</span></div>
|
|
1440
|
+
<h2>Where we landed.</h2>
|
|
1441
|
+
<div class="kpi-grid">
|
|
1442
|
+
<div class="kpi"><div class="num">1,000+</div><div class="label">access-control gaps detected and proposed for fix</div></div>
|
|
1443
|
+
<div class="kpi"><div class="num">~100</div><div class="label">teams adopted (more MRs in flight)</div></div>
|
|
1444
|
+
<div class="kpi"><div class="num">5×</div><div class="label">faster to serve sensitive data</div></div>
|
|
1445
|
+
<div class="kpi"><div class="num">N+ → 1</div><div class="label">audit trails consolidated</div></div>
|
|
1446
|
+
</div>
|
|
1447
|
+
</section>
|
|
1448
|
+
|
|
1449
|
+
<!-- 14. CLOSING -->
|
|
1450
|
+
<section class="slide closing-page" data-notes="One takeaway. Access control at scale isn't a security problem — it's a coordination problem. And the schema is the contract. That's the talk. Thanks for listening. Find me after — I want to hear how your team's handling this.">
|
|
1451
|
+
<div class="kicker">Editorial <span class="folio">¶ 14</span></div>
|
|
1452
|
+
<div class="lede">
|
|
1453
|
+
<div class="big-text" style="margin-bottom: 1.6rem;">
|
|
1454
|
+
Access control at scale isn't a <span class="warn">security problem</span>.
|
|
1455
|
+
</div>
|
|
1456
|
+
<div class="big-text" style="margin-bottom: 1.6rem;">
|
|
1457
|
+
It's a <span class="accent">coordination problem</span>.
|
|
1458
|
+
</div>
|
|
1459
|
+
<div class="big-text">
|
|
1460
|
+
The schema is the contract.
|
|
1461
|
+
</div>
|
|
1462
|
+
<p class="muted" style="margin-top: 1.8rem; max-width: 60ch;">Q&A · find me after.</p>
|
|
1463
|
+
</div>
|
|
1464
|
+
<div class="signoff">
|
|
1465
|
+
<span>— M.H., Amsterdam, 2026</span>
|
|
1466
|
+
</div>
|
|
1467
|
+
</section>
|
|
1468
|
+
|
|
1469
|
+
</main>
|
|
1470
|
+
|
|
1471
|
+
<footer class="deck-chrome">
|
|
1472
|
+
<div class="right">
|
|
1473
|
+
<button onclick="toggleTheme()" title="Toggle theme (L)">L · Edition</button>
|
|
1474
|
+
<button onclick="toggleNotes()" title="Toggle speaker notes (N)">N · Notes</button>
|
|
1475
|
+
<button onclick="toggleHelp()" title="Help (?)">?</button>
|
|
1476
|
+
<button onclick="document.documentElement.requestFullscreen()" title="Fullscreen (F)">F · Full</button>
|
|
1477
|
+
</div>
|
|
1478
|
+
</footer>
|
|
1479
|
+
|
|
1480
|
+
<div class="notes-panel" id="notes-panel">
|
|
1481
|
+
<h4>Speaker notes</h4>
|
|
1482
|
+
<p id="notes-content"></p>
|
|
1483
|
+
</div>
|
|
1484
|
+
|
|
1485
|
+
<div class="help-overlay" id="help-overlay" onclick="toggleHelp()">
|
|
1486
|
+
<div class="panel" onclick="event.stopPropagation()">
|
|
1487
|
+
<h3>Keyboard shortcuts</h3>
|
|
1488
|
+
<table>
|
|
1489
|
+
<tr><td>← / →</td><td>Previous / next slide</td></tr>
|
|
1490
|
+
<tr><td>Space / PgDn</td><td>Next slide</td></tr>
|
|
1491
|
+
<tr><td>Home / End</td><td>First / last slide</td></tr>
|
|
1492
|
+
<tr><td>F</td><td>Toggle fullscreen</td></tr>
|
|
1493
|
+
<tr><td>L</td><td>Toggle Day / Night edition</td></tr>
|
|
1494
|
+
<tr><td>N</td><td>Toggle speaker notes</td></tr>
|
|
1495
|
+
<tr><td>?</td><td>Show / hide this help</td></tr>
|
|
1496
|
+
<tr><td>P / Ctrl-P</td><td>Print mode (one slide per page)</td></tr>
|
|
1497
|
+
</table>
|
|
1498
|
+
</div>
|
|
1499
|
+
</div>
|
|
1500
|
+
|
|
1501
|
+
<script>
|
|
1502
|
+
const slides = document.querySelectorAll('.slide');
|
|
1503
|
+
let cur = 0;
|
|
1504
|
+
|
|
1505
|
+
function pad(n) { return String(n).padStart(2, '0'); }
|
|
1506
|
+
|
|
1507
|
+
function show(i) {
|
|
1508
|
+
cur = Math.max(0, Math.min(slides.length - 1, i));
|
|
1509
|
+
slides.forEach((s, idx) => s.classList.toggle('active', idx === cur));
|
|
1510
|
+
const notes = slides[cur].dataset.notes || '(no speaker notes)';
|
|
1511
|
+
document.getElementById('notes-content').textContent = notes;
|
|
1512
|
+
history.replaceState(null, '', '#' + (cur + 1));
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function next() { show(cur + 1); }
|
|
1516
|
+
function prev() { show(cur - 1); }
|
|
1517
|
+
|
|
1518
|
+
function toggleTheme() {
|
|
1519
|
+
const t = document.body.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
1520
|
+
document.body.dataset.theme = t;
|
|
1521
|
+
localStorage.setItem('deck-theme', t);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function toggleNotes() {
|
|
1525
|
+
document.getElementById('notes-panel').classList.toggle('show');
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function toggleHelp() {
|
|
1529
|
+
document.getElementById('help-overlay').classList.toggle('show');
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
document.addEventListener('keydown', (e) => {
|
|
1533
|
+
if (e.target.tagName === 'INPUT') return;
|
|
1534
|
+
switch (e.key) {
|
|
1535
|
+
case 'ArrowRight':
|
|
1536
|
+
case ' ':
|
|
1537
|
+
case 'PageDown':
|
|
1538
|
+
next(); e.preventDefault(); break;
|
|
1539
|
+
case 'ArrowLeft':
|
|
1540
|
+
case 'PageUp':
|
|
1541
|
+
prev(); e.preventDefault(); break;
|
|
1542
|
+
case 'Home': show(0); break;
|
|
1543
|
+
case 'End': show(slides.length - 1); break;
|
|
1544
|
+
case 'f': case 'F':
|
|
1545
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
|
|
1546
|
+
else document.exitFullscreen();
|
|
1547
|
+
break;
|
|
1548
|
+
case 'l': case 'L': toggleTheme(); break;
|
|
1549
|
+
case 'n': case 'N': toggleNotes(); break;
|
|
1550
|
+
case '?': toggleHelp(); break;
|
|
1551
|
+
case 'Escape':
|
|
1552
|
+
document.getElementById('help-overlay').classList.remove('show');
|
|
1553
|
+
document.getElementById('notes-panel').classList.remove('show');
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// Touch swipe navigation (mobile)
|
|
1559
|
+
let tStartX = 0, tStartY = 0, tStartTarget = null;
|
|
1560
|
+
document.addEventListener('touchstart', (e) => {
|
|
1561
|
+
const t = e.changedTouches[0];
|
|
1562
|
+
tStartX = t.screenX;
|
|
1563
|
+
tStartY = t.screenY;
|
|
1564
|
+
tStartTarget = e.target;
|
|
1565
|
+
}, { passive: true });
|
|
1566
|
+
document.addEventListener('touchend', (e) => {
|
|
1567
|
+
const t = e.changedTouches[0];
|
|
1568
|
+
const dx = t.screenX - tStartX;
|
|
1569
|
+
const dy = t.screenY - tStartY;
|
|
1570
|
+
if (Math.abs(dx) < 60) return;
|
|
1571
|
+
if (Math.abs(dy) > Math.abs(dx) * 0.7) return;
|
|
1572
|
+
// skip if swipe started inside a horizontally scrollable element
|
|
1573
|
+
let el = tStartTarget;
|
|
1574
|
+
while (el && el !== document.body) {
|
|
1575
|
+
if (el.scrollWidth > el.clientWidth) {
|
|
1576
|
+
const cs = getComputedStyle(el);
|
|
1577
|
+
if (cs.overflowX === 'auto' || cs.overflowX === 'scroll') return;
|
|
1578
|
+
}
|
|
1579
|
+
el = el.parentElement;
|
|
1580
|
+
}
|
|
1581
|
+
if (dx < 0) next(); else prev();
|
|
1582
|
+
}, { passive: true });
|
|
1583
|
+
|
|
1584
|
+
const saved = localStorage.getItem('deck-theme');
|
|
1585
|
+
if (saved) {
|
|
1586
|
+
document.body.dataset.theme = saved;
|
|
1587
|
+
}
|
|
1588
|
+
const startIdx = parseInt(location.hash.slice(1)) - 1;
|
|
1589
|
+
if (!isNaN(startIdx) && startIdx >= 0 && startIdx < slides.length) show(startIdx);
|
|
1590
|
+
else show(0);
|
|
1591
|
+
</script>
|
|
1592
|
+
</body>
|
|
1593
|
+
</html>
|