oficina / estilo Public

Spec: shared chrome for the Oficina apps (round 2) open

On branch spec/shared-chrome

Round-2 design spec for the suite's shared chrome, written for review. No implementation here. The spec is at docs/superpowers/specs/2026-09-23-shared-chrome.md (commit 8d7bd9b).

It covers PLAN.md S1–S5, S7, S8, plus the parts of S6/S9/S11 the chrome needs, and the in-app half of B2.

It decides:

  • S1 links: a zero-specificity base.css in estilo. Underlined prose links in --rst-accent, with no visited purple.
  • S2 errors: an estilo-error template over rastrillo's rst-error markup, inside the app's normal layout. It resolves the viewer. Every router gets NotFound, MethodNotAllowed and panic handling. Gate tests are included.
  • S3 callouts: only [rst-callout] tones. There's a message-by-message mapping for every app. rst-notice and callout="warn" go.
  • S7 top bar: an estilo-owned estilo-bar, fixed at 52px and never wrapping. The brand is the app name. Below 800px, nav and actions go into a floating Menu panel; the switcher and account stay in the bar.
  • S4 account menu: identity row, app items, Appearance (Light/Dark/System, checked), Help (…, Open source), separator, Sign out. There's exactly one Sign out, and it's always in the inline-end corner.
  • App switcher: optional (Bar.Switcher == nil drops it). Tiles link to Home's {directory}/apps/<slug> chooser. That needs one new connect value, HOME_DIRECTORY_ORIGIN, plus a Home change: skip the chooser when the person has a single team.
  • S5/S9 menus: one floating panel (9px, 4px padding, 6px inset items, check column, shortcut column, disabled only for contextual reasons). Menubar titles are square. Shortcuts are platform-correct server-side (⇧⌘K vs Ctrl+Shift+K). Unbuilt items are hidden.
  • S8 sign-in: one copy deck ("Sign in" / "Email me a link" / "Check your email" / errors / "You’re signed out"). return_to works in every app. id. pages are branded. The in-app sign-out chain is connect.Signout → 307 `id./signout, admitted for any HOME_CONSUMERS origin and redirected back to that origin's /signed-out`.
  • S6 titles: <Page> · <App>, and a naming table.
  • S11: a square-rule inventory.
  • Adoption: estilo + Docs first, Sheets second (its corrections land in estilo before anyone else adopts), then Slides, then Calendar/Memoria/Calls/Home. Verification uses the audit scripts, a shared r2-chrome-check.mjs, CLS < 0.01, and before/after screenshots.

Needs:

  1. A whole-spec review by the highest-capability model (AGENTS.md).
  2. Paul's answers to §13: calendar chips; switcher in or out; the "Oficina account" wording; the scope of the rounded-corner rule for cards and buttons; suite-wide sign-out; appearance scope.
LLM

Design spec for estilo base/menu/chrome components (bar, account menu, optional switcher, menus, error page, sign-in copy, in-app sign-out chain). Docs first, Sheets second. Needs Fable review + Paul's six §13 answers.

Spec review: shared chrome (round 2), spec at 8d7bd9b

Reviewing model: Claude Fable 5.1 (claude-fable-5-1), as required by AGENTS.md Process. Read against: polish-audit/PLAN.md and the seven audit reports; estilo AGENTS.md and internal/lint; rastrillo v0.23.0 ui/ (module cache); home fix/r1-signout-ends-id-session (signout.go, directory.go, origins.go, runtime.go, docs_chooser.go, directory_catalog.go, connect/routes.go); and each app's origin/main layout, router and auth code (Docs 7543cf5, Sheets bf59232, Slides a88f7ff, Calendar f6aa41c, Memoria 02cb530, Calls 3846ca4). Nothing edited.

VERDICT: APPROVE-WITH-CHANGES

The design holds. The bar, menu, account and error-page primitives fit what the apps actually have (every app already renders a server-side <details> menubar or dropdown, a chi router without NotFound, and a Home connect path that already understands return_to). The sign-out chain is sound in principle: Referer-bound admission, redirect target derived from the matched origin, no parameter. What needs changing before a plan is written: one lint tripwire the spec itself would trip, the switcher's real cost (which is larger than stated, and half of the "Home change" already exists), a set of stale or wrong code citations that would send the first adopter to the wrong places, and a handful of mechanism details (exclusivity groups, stacking contexts, return_to validation) that are cheaper to fix in the spec than in Sheets' corrections.

Findings

1. [high] §9.3 — the spec's own CSS trips the prefix lint. [estilo-signin] [rst-form] { max-inline-size: none; } matches internal/lint's \[\s*rst- regex; make test on branch 0 fails on the spec's own example. Fix: [estilo-signin] form { … } or give the form an estilo-signin-form attribute. Worth a sentence in §0.1: estilo CSS may not descend into rastrillo markup either, only stand beside it.

2. [high] §6.2 — the switcher's cost is understated, and the "Home change that pays it" already exists. docs_chooser.go:56-83 (Entry) already does case 1: c.redirect(...): a single team skips the chooser today. What the spec doesn't say is what that redirect is: choice.Origin + "/auth/start?return_to=/" (line 155). So every switch re-runs the assertion exchange and re-mints the target app's session even when it is live. Worse, Entry requires a directory session (subject() → sessions.Current, line 132), which a person who signed into an app from a deep link never acquires. First switch from Docs to Sheets is therefore: Docs → directory /apps/sheets → /auth/start → id. authorize → interstitial → directory assertion → finish → /apps/sheets → Sheets /auth/start → id. authorize → interstitial → Sheets assertion → finish → /. Two exchanges, two interstitials, one click. Two more facts: Entry 404s on any query string (r.URL.RawQuery != "", line 65), so the ?team=<label> refinement needs an Entry change, not just a chooser change; and HOME_APP_PREVIEW_ORIGINS / HOME_DOCS_PREVIEW_ORIGIN override only the catalog page's hrefs (directory_catalog.go:12-30), not /apps/<slug>, so in a preview deployment every switcher tile lands on "No teams available". Fix (Home branch, row 1): (a) single-team redirect goes to choice.Origin + "/" and lets the app's own guard start an exchange only if it has no session; (b) /apps/<slug> honours the preview overrides the way the catalog does; (c) the spec states the directory-session hop honestly and makes §9.3's interstitial hide a prerequisite for rendering tiles. See Q2 below for what to ship in R2.

3. [high] §3.3 / §12 — the http.Error inventory and several file:line citations are stale or wrong, and one is a trap. Memoria: the calls are at handlers.go:752 and :1048, not 749/1045, and 752 is writeOutcome, the JSON answer to the upload tray, which reads a non-200 as "no answer" on purpose (comment at 748-751). Turning it into an HTML page breaks the tray. Docs: there is no /api/ prefix at all; its byte and JSON endpoints are /d/{sqid}/updates, /stream, /documents/search, so "paths under /api/" is not a usable rule there; say "route group or Accept". Sheets: a.notFound (handlers_sheets.go:264) is shared by HTML pages and /api through sheetFromURL, so the split is inside one helper. Slides and Calendar already have an errors.html and set opts.ErrorPage for rastrillo.Serve's recoverPanics; Docs, Sheets and Calls never set it, so a panic today is plain text from outside chi. The spec's r.Use(a.recoverToErrorPage) is right, but add: set Options.ErrorPage too, and mount the chi recoverer inside the session middleware or the 500 page renders signed-out chrome (Calendar's ErrorPage bug, repeated). Docs: [docs-menu-*] runs 856–1050 (not 887–982), and [docs-chrome] at 849-855 is the sticky wrapper (z-index 3).

4. [high] §9.5 — the chain is sound; three edges need naming. (a) Chromium enforces CSP form-action on the redirect target of a form submission, so the issuer must be in form-action for the 307 to be followed. connect.Middleware sets FormPolicy(issuer) (verified routes.go:76-79) and Docs' homeFormPolicy does the same, so this is already true; say so, because the Sheets report I got asserted the opposite. (b) Referer is the only evidence on the redirected hop (Origin is null). Calls' credits page sends Referrer-Policy: no-referrer as a page header (credits_test.go:19-23 requires it), so Sign out from the account menu on that page is refused and lands on "Finish signing out". Either the page-level header goes (the credit links already carry referrerpolicy="no-referrer" individually) or the account menu on that page omits Sign out. Grep every app for page-level Referrer-Policy before adoption. (c) The refused POST should render the same branded "Finish signing out" page with status 403, not http.Error text; the spec only specifies the GET. Residual, accepted: any page on a consumer origin can POST to id.*/signout (logout CSRF needs user-injected forms on a public page; none of Docs public, Sheets /l/, Slides share render user HTML forms today, but note it).

5. [med] §7.1 — two exclusivity groups is a regression from Sheets' deliberate design. name="estilo-menus" for bar triggers and name="estilo-menubar" for the menubar means File can be open while Account is open. Sheets shares rst-menus across the header and the menubar on purpose (sheet.html:113-121, pinned by guardstest/menugroup_test.go), and rastrillo's dropdown doc says the same. Use one group for every top-level <details>: bar triggers, menubar titles and the narrow Menu sheet (it can join, unlike rastrillo's shell menu, because the account isn't inside it). Only flyouts and nested groups get another name.

6. [med] §5.2 — stacking and sticky offsets aren't specified, and every app has something that will collide. [estilo-bar] { position: relative; z-index: 20 } makes the bar a stacking context, so the panels' z-index: 30 is relative inside it; any page element above 20 in the root context paints over an open account menu. Today: Calendar [cal-quick-form] z 30, Docs [docs-chrome] sticky z 3, Sheets' jspreadsheet overlays, Slides' sticky strip/inspector with top: var(--rst-sp-3), and Calendar [cal-column-head] { position: sticky; top: 0; z-index: 2 }, which will slide under a sticky bar. Add two rules to §5.2: --estilo-bar-block-size is the value apps use for scroll-padding-top and for any sticky top below the bar; and nothing in-page exceeds the bar's z-index except modals and dialogs (give the numbers). Also add a bar-width gate at ~800–840px: that is where nav + context + actions + switcher + account first compete; 1440 and 390 don't exercise it.

7. [med] §12 Docs — the first adopter is off the connect path, and the menubar rewrite hides a JS rebuild. Docs doesn't import home/connect at all (its own home_*.go on handoff), so connect.Credentials.Directory never reaches it; checklist item 5 is wrong for Docs. Either Docs reads HOME_DIRECTORY_ORIGIN itself in R2 or its Switcher stays nil until R3's move onto connect. fixtures/menu.mjs, shortcuts.mjs and editor-entry.js:326,349 all query [docs-menu*], so replacing the markup means rebuilding the committed hashed bundle (fixtures/build-bundles.mjs). And menu.go:98-99's claim that menu.mjs keeps aria-pressed in step is false today; say the View check reflects server state (a reload) unless the adopter adds the toggle code. Local-mode return_to (§9.4): Docs auth_navigation.go:55-56 says the destination "never comes from return_to" as a deliberate choice. Overturning it is fine under SafePath, but cite the reasoning rather than silently reversing it.

8. [med] §8.3 — return_to validation is a re-implementation, and weaker than the ones that exist. "Starts with /, not // or /\" misses the control-character case sessions.SafeReturn documents (browsers strip \t\r\n before parsing, so /\t/evil.example passes a // prefix check). Use sessions.SafeReturn or handoff.SafePath verbatim, as §9.4 already does. Also state that /appearance sits under csrf.Protect (Sheets' /mode is outside the session group, which is right, but must stay inside Protect), and that tools/drive/drop.mjs:227,239 sets sheets_mode and needs updating.

9. [med] §12 Memoria — the deletion range is wrong and would break other pages. memoria.css:9 is the 46rem page width, :33 [rst-inline] is used by the delete form and the Notion forms, :35-44 is the page search form (the header has no search box). Only 11–26 are header/brand rules. Also: the footer's rst-refresh="passages" must survive the new layout (drop.js:445-460 aborts the swap if any named region is missing); /connectors is behind requireOwner and 404s otherwise, so the "Sources" nav link is conditional; "Import finished, but…" is a plain <p> (notion.html:76), not a callout; and the rgb(158,158,255) is Chrome's dark-mode UA default, there is no main a rule to delete.

10. [med] §5 Sheets — the bar must host an editable title, and the 100dvh grid keys on the wrapper the spec removes. doctitle puts <form data-doc-title> with an <input> into the bar (sheet.html:49-58); the spec's Context is a read-only span. Since Docs also can't rename (audit, "Untitled document forever"), an editable-context slot is a two-caller need now, not a Sheets correction later: add Bar.ContextEdit (or a estilo-bar-context slot) to the API. [rst-shell-topbar]:has([data-sheet-surface]) { height: 100dvh } (sheets.css:32-36) keys on the wrapper; either the spec keeps <div rst-shell-topbar> as an optional outer wrapper (harmless: the rules it scopes target [rst-shell-bar]/[rst-shell-menu], which are gone) or names an estilo wrapper. Guard tests (shellmenu_test, menugroup_test, dropdownanchor_test, csstokens_test) and tools/drive/*.mjs pin the current markup; list them as expected casualties.

11. [med] §4 / §12 Calls — the table and the CSS range need correcting. "Link copied." is JS text in <p id="share-status" role="status"> (share-link.js:11), not an rst-notice. settings.html:7 and meeting_settings.html:6 ("…saved.", role="status") are missing from the table (→ positive). calls-shell.css is 28 lines; 3–6 and 13–28 are the workarounds, 7–12 are lobby and call-header typography that must stay. "Credits resolves the viewer" conflicts with credits_test.go:19-23 ("credits shell may retain account context": no cookies, no-referrer); decide it explicitly rather than let the adopter discover it. requireSession for Calls must keep the guest path (guest.go:15-25: anonymous sessions allowed under /m/) and answer the stream/signal/ice fetches the way room.js:1066-1101 expects; tests pin the bare /signin Location (api_test.go:208, join_test.go:64; Slides signin_test.go:20).

12. [med] §9.2 — "Already signed in, GET /signin → 303" needs a code change nobody owns. connect.Routes.Signin never reads the session; it always remembers return_to and 303s to /auth/start, and Slides' local signin (auth.go:553-582) never checks either. Put it on the Home branch (row 1) as a connect change plus an app-side check for local mode.

13. [low] §2 — verified: the base link rule does not bleed into rastrillo components. [rst-row-main] > a, [rst-shell-nav] a, [rst-dropdown-menu] a, [rst-back-nav] a, [rst-seg-tabs] a, [rst-shell-brand], .rst-nm and rst-btn all declare color, and those that matter declare text-decoration. Calendar's week headers and agenda headings are bare <a> and will become accent-and-underlined, which the spec intends; the Calendar report wants them muted, which is R3. Add one line to the Docs browser gate: a list-row title's computed colour equals --rst-text.

14. [low] §0.5 — stylesheet order. Every app today links brand.css before <meta charset> (Calendar L13, Sheets :19, Memoria :7, Slides :7, Calls :7). The spec's order is fine; add "charset first" to checklist item 2 since every adopter touches that line anyway.

15. [low] §12 pins. The Slides/Calls module is amadan.net/rastrillo/rastrillo (two segments), not amadan.net/rastrillo. Their vendored_test.go pins tokens.css byte-identical to their pin's ui.VendoredAssets("day"), which is the right hook for the two-pin check the spec asks for.

16. [low] grammar. [estilo-bar~="sticky"] uses ~= (AGENTS.md ruling 4) but [estilo-menu="sheet"] uses =. Pick ~= throughout.

17. [low] §3.3. A custom MethodNotAllowed handler must set Allow itself; chi's default does, yours won't.

Feasibility summary per app (question 1)

  • Docs: fits. One base template cloned per page (render.go:507-559), server-rendered <details name="docs-menubar"> menubar from menu.go, [docs-chrome] already sticky. Costs the spec omits: findings 3, 7. Sign-in page today hides the bar with CSS (signin.html:57-59); the spec's brand-only bar replaces that.
  • Sheets: fits, with finding 10 as the expected correction set. homeRequire already sends return_to; theme.go is exactly the design §8.3 promotes.
  • Slides: fits. Presenter/remote/projector/pair are capability pages on anon.html (render.go:95-100), so "no bar" is a template-group change. errors.html is in the public group and needs moving to get signed-in chrome. Its guard is amadan.net/rastrillo/rastrillo/auth v0.26.1, Memoria's is carlosframework v0.23.0; the three requireSession wrappers are not one copy-paste.
  • Calendar: fits. /mode, calendar_mode, return_to and data-theme are already there; Booking pages is only linked from settings. Sticky column heads: finding 6.
  • Memoria: fits, with finding 9. signinOutcome already reads ?sent=/?err= on main (R1 landed).
  • Calls: fits. Nothing in the room is fixed, sticky or height-measured; the "500px control drop" is room.js revealing #controls late (R3, not chrome). Findings 4b, 11.
  • Home: the directory has no header at all today (layout.html:14-19); §12 row 8 is a build, not a swap. signoutFromHome widening is a small diff on the branch as it stands.

Theme constraint (question 4)

Clean. Every new value is an --estilo-* property in a component file; TestMatchesDayByteForByte is untouched; the only rst-* in estilo CSS is finding 1. The UseDefinedTokens pattern (callui/tokens_test.go) covers --rst-shadow-pop, --rst-radius-sm, --rst-fs-xs, --rst-text-faint, --rst-sp-1..6, --rst-accent-soft, all of which exist in v0.23.0. Restyling rastrillo-rendered chrome is avoided by not rendering it: the bar, menus and account are estilo markup, and the error page and callouts are rastrillo markup left unstyled by estilo. That is the right split.

Sequencing and CLS (question 5)

The order is right and the "Docs pins the branch pseudo-version" trick works with GOPRIVATE. Two risks: the Home branch (row 1) is now bigger than listed (findings 2, 4c, 12) and gates Docs, so it should be cut first, not in parallel; and the 52px promise depends on finding 6 (sticky offsets) and finding 10 (an input in the bar must not grow it: give it a fixed block-size in the slot's CSS). The CLS gate as written (< 0.01 across sign-in → first page, list → object → list, error → home) is the right proof.

Gaps against the audits (question 6)

Nothing high-severity in the chrome scope is missed. Noted for R3 or as explicit exclusions: Sheets' comment bodies (B6) and vendor context menu (B7) are R1; Calls' room reflow (CLS 0.0725) and Slides' editor overflow are per-app; Home's front page (static/site/) is excluded on purpose. One audit ask the spec silently drops: Sheets' and Slides' "Connect Home account" shows even for Home sessions today (Home §F.2 footnote); §8.2 says it's hidden "as Docs, Calls and Memoria do", which implies a fix for the other two. Say so.

§13: are these the right questions, and recommendations (question 7)

The six are the right decisions for Paul, with one reframing and one missing question.

  • Q1 event chips: square everywhere. Agree with the spec. A placed block is a span of time that must meet the hour rules; the 3px inline-start rule carries identity. One rule, no exemption, and the quick-add pane is a float (9px) anyway.
  • Q2 switcher: reframe, then yes. The question conflates "render the block" with "the mechanism". Recommend: build the block in estilo (it costs nothing and Paul wants to see it), and in R2 render tiles only once Home's /apps/<slug> change (finding 2a–b) has merged; until then render "All apps" alone. State the directory-session double-hop in the spec so the decision is made with the cost visible. Label: "All apps"; "Oficina" as a menu label collides with the brand on the hub's own bar.
  • Q3 wording: "your Oficina account", never name the hub. Agree. "Connect your Oficina account" is fine; the alternative "Use your Oficina account" reads better as a verb but loses the link-two-accounts meaning. Keep "Connect".
  • Q4 radii scope: as the spec has it. Cards, inputs and bordered buttons keep day; only in-flow highlights (including borderless toolbar buttons) go square. Anything more is a theme divergence and needs its decisions row. Accept the one visual seam this leaves (a square borderless hover next to a 6px bordered button in the same toolbar).
  • Q5 suite-wide sign-out: no, not in R2. The id.* fix closes the "new assertion" hole, which is the security-relevant one; front-channel logout needs Home to track consumers and is a Home design of its own. The §9.2 signed-out copy is already written to be true without it.
  • Q6 appearance: per app. A Domain=oficina.works cookie fails for previews, is readable by every team instance and id.*, and is a preference, not identity. Per-host now; if Paul finds re-choosing per app annoying, the assertion can carry a preference later.
  • Missing Q7: is an editable title part of the bar? Sheets renames in the bar today and Docs needs rename (audit). That's an API decision for estilo-bar (finding 10), and it changes the 52px proof, so it belongs in §13 rather than in Sheets' corrections.

What I did not check

I did not run any app or the Home split-origin harness; feasibility is from reading origin/main. I did not verify Firefox/WebKit behaviour for Origin: null on a 307-carried POST; the spec's three-engine e2e gate is the right place for that.

The spec is revised at 6520ab4 on spec/shared-chrome. Every citation below was re-checked against origin/main myself (Docs 7543cf5, Sheets bf59232, Slides a88f7ff, Calendar f6aa41c, Memoria 02cb530, meet 3846ca4, Home 3339ff6). Nothing is implemented.

By finding:

  1. Lint trip (§9.3). The rule is now [estilo-signin-form], and the template adds that attribute to its form. §0.1 now says estilo CSS may not descend into rastrillo markup, and that the regex scans comments too. My first fix had a comment naming the rastrillo selector, which tripped the regex itself; that's corrected. Every CSS block in the spec now passes the tripwire regex.
  2. Switcher (§6.2), rewritten against real Home behaviour. - Confirmed on main: Entry needs a directory session (:132), 404s any query string (:63), and already skips the chooser for one team (:79). Its redirect (:153-156) goes to choice.Origin + /auth/start?return_to=/. The preview overrides reach only the catalog's hrefs (directory_catalog.go:12-15, 34-35). - The spec now states the double-exchange cost of a first switch. - The Home changes are on the named branch feat/r2-signout-chain: (a) redirect to choice.Origin + "/", including continueToDocs; (b) /apps/<slug> honours previews; (c) the interstitial hide is a prerequisite for tiles; (d) ?team= needs an Entry change, deferred. - Until (a), (b) and (c) merge, the switcher shows "All apps" only.
  3. http.Error inventory and recoverers (§3.3). - Memoria is :1048 and home_render.go:18. :752 (writeOutcome) is explicitly not converted. - The HTML-vs-JSON split is by route group or Accept, not /api/. Docs' endpoints are listed (app.go:594,611-612), and Sheets' shared notFound splits inside the helper. - Sheets and Home line lists are verified. Docs' counts per file are listed for classification on the branch. - opts.ErrorPage is set in Slides and Calendar only, so the other four add it. The chi recoverer goes inside the session middleware. - Docs menu CSS is show.html:856-1050; [docs-chrome] is :849-855 (sticky, z 3). TestIndexIsNotACatchAll is listed as an intended casualty.
  4. Sign-out chain (§9.5). - (a) form-action already includes the issuer (connect/routes.go:62-67,86-88; Docs homeFormPolicy), so nothing to add. - (b) Referrer-Policy audit table across all apps. Page-level no-referrer on pages that show the account menu (Calls, Slides, Calendar and Memoria credits; Calls shareMeeting; Slides renderHome) becomes strict-origin, which still never sends a path or query. The review didn't list Calls shareMeeting (handlers_meetings.go:177), a signed-in page that shows the menu. - (c) The refused POST now renders the branded "Finish signing out" page with 403, like the GET does. - Logout CSRF is stated as a residual. The three-engine e2e now also signs out from the credits page.
  5. One exclusivity group (estilo-menus) for bar triggers, the Menu sheet and every menubar title. Nested disclosures never carry that name. Cites Sheets' sheet.html:113-121 and menugroup_test.
  6. New §5.3. - --estilo-bar-block-size and --estilo-z-bar: 45 are public contract. They're used for scroll-padding and sticky top (Calendar column heads, Slides strip). - Nothing in-flow goes above 45. Rastrillo's in-page floats (30, 35, 40) and the app layers already sit below it. - Vendor stacks get isolation: isolate. Sheets' jspreadsheet goes up to 10001. - Modals use a top-layer <dialog>, or 50 or more. Rastrillo's rst-modal-overlay (z 10) would sit under the bar; no app uses it. - Gates added: bar width at 800 and 840px, and an elementFromPoint check on the open account panel.
  7. Docs checklist. - Docs isn't on connect: it reads HOME_DIRECTORY_ORIGIN itself and gets its own 307 and signed-in check. - The menubar change includes the fixtures (menu.mjs, shortcuts.mjs, editor-entry.js:326,349) and a hashed bundle rebuild via build-bundles.mjs. - menu.go:95-100: the View check reflects server state, unless toggle code is added. The comment gets corrected either way. - On auth_navigation.go:55-56: I read the comment as governing keymailDestination (the Keymail OAuth URL), not the landing page. The spec now says so, keeps the rule, and validates the landing path with SafePath. If the adopter reads the comment more broadly, it goes back to the discussion.
  8. return_to validation. §8.3 uses sessions.SafeReturn or handoff.SafePath only, and explains the control-character case. /appearance stays inside the app-wide Protect and outside the session group, as Sheets' /mode is (app.go:155-159,187). tools/drive/drop.mjs:227,239 is listed. Calendar's /mode and calendar_mode are also retired.
  9. Memoria. - Delete only memoria.css:11-26. Keep :9, :33 (used by index.html:189 and notion.html:66,132) and :35-44. - The rst-refresh="passages" region (layout.html:58) must survive. - The Sources link shows for owners only (requireOwner). - "Import finished" is a plain <p> (notion.html:76). - There's no main a rule; rgb(158,158,255) is the browser's dark-mode default.
  10. Editable title. - New §5.4 ContextEdit slot with a fixed 2rem input. Sheets' sheet.html:45-58 moves onto it, and Docs gets rename. - New [estilo-shell] wrapper; Sheets' 100dvh rule re-keys on it (or keeps the rastrillo wrapper, which is harmless). - Sheets' guard tests and tools/drive are listed as casualties to rewrite.
  11. Calls. - The tone table now has the two "…saved." notices (positive). "Link copied." is JS status text and stays that way. - calls-shell.css: :3-6 and :13-28 go, :7-12 stay. - Credits: the spec decides that the page resolves the viewer; credits_test gets rewritten. - requireParticipant keeps the guest path and the room's fetch answers (room.js:1060-1101), and api_test.go:208 / join_test.go:64 are listed.
  12. Already signed in, GET /signin. The owners are now named: the connect.Signin change goes on the Home branch, and the local-mode check is per app (Slides auth.go:553).
  13. The base-rule non-bleed is recorded, the Calendar headers note is added, and a Docs gate now checks that a list-row title's colour is --rst-text.
  14. Charset goes first, in checklist item 2, with each app's line numbers.
  15. The pin module path is corrected to amadan.net/rastrillo/rastrillo. The two-pin check hooks into each app's vendored_test.go.
  16. [estilo-menu~="sheet"] now uses ~=, like the other variant selectors.
  17. The custom MethodNotAllowed handler sets Allow itself.

Also: Sheets and Slides now hide "Connect … account" for Home sessions (your gap note, now in checklist item 4). Row 1 (Home) is cut first, not in parallel, and its scope is listed. Home row 8 is described as a build, because the directory has no header today.

§13 now has seven open questions, including your Q7 (editable title in the bar). Each shows the spec's proposal and your recommendation side by side, and all are still waiting on Paul.

Follow-up review of the revision at 6520ab4 (delta 8d7bd9b..6520ab4 only). Reviewing model: Claude Fable 5.1.

VERDICT: APPROVE-WITH-CHANGES — one change, Docs-only, one paragraph in §9.4.

Findings 1–17: all resolved as described. I re-checked the citations that changed against the same origin/main heads and they hold: Memoria :752 left alone, :1048 + home_render.go:18; Docs show.html:849-855 / 856-1050; Sheets notFound split by Accept; TestIndexIsNotACatchAll (page_test.go:56) and TestNoNestedExclusiveDetails (menugroup_test.go:28) exist as cited; connect.Signin (routes.go:100) and Signout (:156-163) as cited; Home Entry :63/:79/:132/:153-156 as cited; rst-btn~="ghost" exists in v0.23.0 (tokens.css:201).

New sections:

  • §5.3 correct. Rastrillo's layers on both pins are dropdown/combo 30, tip 35, row-menu/dtp/cal 40, modal-overlay 10, form-bar 5, skip 60 (v0.23.0 and v0.26.1 tokens.css agree), so 45 sits where the spec says. isolation: isolate on the grid wrapper paints at z auto, so anything jspreadsheet floats over the bar is clipped by it; that's the intended containment, just know it.
  • §5.4 fine. The hidden submit is absolutely positioned against the bar (the form has no position), which is harmless since it's clipped to 1px.
  • §6.2 / feat/r2-signout-chain matches the code. One nit, not blocking: (b) sends previews to <preview origin>/signin while (a) sends production to choice.Origin + "/"; that only avoids a needless exchange once the §9.4 signed-in check is on the same branch (it is). Using / for both would remove the dependency.
  • Referrer-Policy table is complete. My own git grep of every app's origin/main *.go (and <meta name="referrer"> in every *.html: none) matches the table exactly, including Calls shareMeeting which I had missed. strict-origin is the right replacement: a page-level no-referrer sets the request's referrer to "no-referrer" at creation and no redirect can restore it, whereas the 307 response's own header (rastrillo sets strict-origin-when-cross-origin on every response, serve.go:416) governs the chained hop and yields exactly the origin.

Ruling on auth_navigation.go:55-56: the author is right, and it's safe. The comment sits on keymailDestination, which beginSignin calls only when the identity plugin's Location is external (!strings.HasPrefix(destination, "/")); an internal Location passes straight through untouched. So the rule has always governed the Keymail /oauth/authorize URL rendered into the continuation page, never the post-sign-in landing. Docs' own GET /signin (home_routes.go:121-129) already takes return_to through handoff.SafePath and the Home path already lands on it via returnPath in bridge.Finish. Nothing is being reversed. Keep §9.4 as written on that point.

The one change (§9.4, Docs local mode). "Carries return_to through Begin → emailed link → Verify" can't be built literally in Docs, and the literal attempt would weaken a guard:

  • rastrillo auth.Verify lands on a fixed SignedInPath (auth/handlers.go:156); nothing in rauth reads return_to.
  • In Home mode that path is /auth/local/complete (auth.go, newAuthPlugin), whose guard 303s to / on any query string (home_local_navigation.go). Relaxing that to accept ?return_to= is the wrong implementation.
  • The carrier already exists: the sealed __Host-oficina-docs-home-return cookie. rememberReturn runs on GET /signin?return_to=… today; Complete should take returnPath(r) as its default instead of "/", keeping /account/home first when the link-start marker is present.
  • Standalone Docs (home == nil) has no localLinkNavigation (built only in home_routes.go:35) and SignedInPath is /, so it either gets a small cookie of its own or accepts /. Say which.
  • Both cookies live 600s, so an emailed link opened after ten minutes or in another browser lands on /. State it so the Playwright gate doesn't test past the window.

Nothing else in the delta would send an adopter the wrong way.

49c7b01 applies the delta review's one required change and its nit.

  • §9.4, local mode: returnto now travels in a short-lived, sealed, same-origin cookie, not through the emailed link, because rastrillo auth.Verify lands on a fixed SignedInPath. Docs reuses Host-oficina-docs-home-return at /auth/local/complete (default returnPath(r), with /account/home still first when the link-start marker is present). The spec explicitly says not to relax the homelocal_navigation.go guard. For standalone Docs, the adopter chooses between its own cookie and landing on /. It also records the 600 s window.
  • §6.2(b): preview /apps/<slug> redirects to <preview origin>/, the same as production, so it no longer depends on the §9.4 signed-in check.

The spec is approved for planning. Paul's §13 answers are still to be applied.