Phase 2 of #4265, following #4300. See the 12-phase split.
Retires the three .less stylesheets that belong to leaf apps — 3 files, 62 LOC, taking src/ from 70 .less files to 67. Each has a small, self-contained QA surface, which is why they are grouped: three screens, individually checkable.
Unblocked as of 2026-08-30 — #4264 merged as 0f869ddaa, so the canonical-palette question these conversions depend on is settled.
The three files, and what is actually live in them
src/apps/marketplace/src/app/view/CustomApp.less (18 LOC) — only .CustomApp is used, at CustomApp.js:22. .InstallApp and .Filter are dead (the InstallApp in that file is a React component import, and the styles.Filter hits elsewhere in src/ belong to other modules). Note the nested iframe rule is load-bearing: it sizes an <iframe> rendered by a descendant component, so it must survive as an & iframe selector, not be flattened onto the container.
src/apps/media/src/app/components/Thumbnail/Loading.less (28 LOC) — .Load and .Loading are both used, from two apps: media/src/app/components/Thumbnail/index.tsx:405,500 and content-editor/src/app/components/FieldTypeMedia.tsx:864, which reaches across app boundaries to import it. Contains a @keyframes shimmer; css-modules scopes keyframe names too, so this needs MUI's keyframes helper rather than a bare string.
src/apps/code-editor/.../Workspace/components/NotFound/NotFound.less (16 LOC) — .NotFound is used, and NotFound.js:7 also uses styles.display, which is not declared in this file. It comes from @import "shell/components/legacy/typography.less", which css-modules inlines into the importing module's class map. Any conversion in this epic has to check for inherited classes like this one; a reader looking only at the co-located stylesheet will miss it.
Colour and type mappings
None of the three literals has an exact token. Per docs/design-system.md §4 step 1 they map to the nearest ramp step, which is a deliberate sub-perceptual change, consistent with what #4264 did across src/:
| literal |
where |
maps to |
ramp value |
#eff1f3 |
shimmer gradient stops 25% and 65% |
grey.100 |
#F2F4F7 |
#e2e2e2 |
shimmer gradient stop 45% |
grey.200 |
#E4E7EC |
#eff5ff (@zesty-white) |
NotFound h1 colour |
blue.50 |
#F0F9FF |
styles.display resolves to typography.less's .display: 32px, line-height 48px, weight 200, letter-spacing 0.32px. Per §2 that is variant="h2" (32px) with fontWeight, lineHeight and letterSpacing overridden to hold the pixels exactly — §2 explicitly permits overriding those three on a variant.
Scope
- Convert the three stylesheets to
sx, delete them, and drop their imports.
- Drop the dead
.InstallApp and .Filter rules rather than porting them.
- Remove
className={styles.IframeContainer} at CustomApp.js:68. .IframeContainer is declared nowhere, so styles.IframeContainer is already undefined and the attribute is already a no-op today — deleting it is behaviour-preserving, not a style change.
- Nothing else. No touching the other 67 stylesheets, and not the
less-loader rule (phase 12).
Acceptance criteria
Verification
Unlike #4300 this phase does change what renders, so a build is not sufficient. For each of the three surfaces, capture getComputedStyle on the affected element before and after and record both columns:
- Marketplace —
/apps/<zuid> for an installed app: the <main> height and the iframe's height/width.
- Media — a thumbnail in its loading state, in the media grid and in a content item's media field:
background-image, background-size, and that animation-name resolves to a running animation.
- Code editor — a not-found view: the container's flex centering and the
h1 colour and font metrics.
Phase 2 of #4265, following #4300. See the 12-phase split.
Retires the three
.lessstylesheets that belong to leaf apps — 3 files, 62 LOC, takingsrc/from 70.lessfiles to 67. Each has a small, self-contained QA surface, which is why they are grouped: three screens, individually checkable.Unblocked as of 2026-08-30 — #4264 merged as
0f869ddaa, so the canonical-palette question these conversions depend on is settled.The three files, and what is actually live in them
src/apps/marketplace/src/app/view/CustomApp.less(18 LOC) — only.CustomAppis used, atCustomApp.js:22..InstallAppand.Filterare dead (theInstallAppin that file is a React component import, and thestyles.Filterhits elsewhere insrc/belong to other modules). Note the nestediframerule is load-bearing: it sizes an<iframe>rendered by a descendant component, so it must survive as an& iframeselector, not be flattened onto the container.src/apps/media/src/app/components/Thumbnail/Loading.less(28 LOC) —.Loadand.Loadingare both used, from two apps:media/src/app/components/Thumbnail/index.tsx:405,500andcontent-editor/src/app/components/FieldTypeMedia.tsx:864, which reaches across app boundaries to import it. Contains a@keyframes shimmer; css-modules scopes keyframe names too, so this needs MUI'skeyframeshelper rather than a bare string.src/apps/code-editor/.../Workspace/components/NotFound/NotFound.less(16 LOC) —.NotFoundis used, andNotFound.js:7also usesstyles.display, which is not declared in this file. It comes from@import "shell/components/legacy/typography.less", which css-modules inlines into the importing module's class map. Any conversion in this epic has to check for inherited classes like this one; a reader looking only at the co-located stylesheet will miss it.Colour and type mappings
None of the three literals has an exact token. Per
docs/design-system.md§4 step 1 they map to the nearest ramp step, which is a deliberate sub-perceptual change, consistent with what #4264 did acrosssrc/:#eff1f3grey.100#F2F4F7#e2e2e2grey.200#E4E7EC#eff5ff(@zesty-white)NotFoundh1colourblue.50#F0F9FFstyles.displayresolves to typography.less's.display: 32px, line-height 48px, weight 200, letter-spacing 0.32px. Per §2 that isvariant="h2"(32px) withfontWeight,lineHeightandletterSpacingoverridden to hold the pixels exactly — §2 explicitly permits overriding those three on a variant.Scope
sx, delete them, and drop their imports..InstallAppand.Filterrules rather than porting them.className={styles.IframeContainer}atCustomApp.js:68..IframeContaineris declared nowhere, sostyles.IframeContaineris alreadyundefinedand the attribute is already a no-op today — deleting it is behaviour-preserving, not a style change.less-loaderrule (phase 12).Acceptance criteria
.lessfiles are deleted and no file imports them.find src -name '*.less' | wc -lreturns 67.& iframedescendant rule still sizes the marketplace iframe.npm run build:devsucceeds andtsc --noEmitintroduces no newsrc/errors.cypress/e2e/media/*andcypress/e2e/code/*recorded for the touched areas.Verification
Unlike #4300 this phase does change what renders, so a build is not sufficient. For each of the three surfaces, capture
getComputedStyleon the affected element before and after and record both columns:/apps/<zuid>for an installed app: the<main>height and the iframe's height/width.background-image,background-size, and thatanimation-nameresolves to a running animation.h1colour and font metrics.