- { __( 'Sign in with a free WordPress.com account to unlock everything Studio offers.' ) }
-
-
-
+
+ { /* Same decorative backdrop as the onboarding shell, so the
+ welcome reads as the start of that flow. */ }
+
+
+
+
+
+
+
+
+
+
+
+ { /* Overlaps the lower part of the toy; the scrim blurs the
+ particles behind the text with a soft top fade. */ }
+
+
+
{ __( 'WordPress Studio' ) }
+
+ { __( 'Your local studio for building WordPress sites, plugins, and themes.' ) }
+
+
+
+
+
+
+
+
+ saveUserPreferences.mutate( {
+ analyticsEnabled,
+ source: { surface: 'onboarding' },
+ } )
+ }
+ />
+
+
+
-
- saveUserPreferences.mutate( {
- analyticsEnabled,
- source: { surface: 'onboarding' },
- } )
- }
- />
+
+ { createInterpolateElement(
+ __(
+ 'By continuing, you agree to our Terms of Service and have read our Privacy Policy.'
+ ),
+ {
+ tos_link: (
+
+
+
+ { authUser ? (
+ <>
+
+
+ { authUser.displayName }
+
+
logout.mutate() }
+ >
+ { __( 'Log out' ) }
+
+ >
+ ) : (
+
+ ) }
+
);
diff --git a/apps/ui/src/ui-classic/router/route-welcome/style.module.css b/apps/ui/src/ui-classic/router/route-welcome/style.module.css
index 42d5cce564..2d1a064b69 100644
--- a/apps/ui/src/ui-classic/router/route-welcome/style.module.css
+++ b/apps/ui/src/ui-classic/router/route-welcome/style.module.css
@@ -1,65 +1,269 @@
-.root {
+/* Full-window welcome canvas: centered content above a real footer row
+ (legal left, actions right). The footer is a flex sibling rather than
+ pinned, so a short window shrinks the content instead of letting it
+ collide with the legal text — the app can be resized to 600px tall. */
+.page {
+ /* No global border-box reset in this app — without it the padding adds
+ to the 100vh and the document scrolls, pushing the footer offscreen. */
+ box-sizing: border-box;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ padding: 40px 50px 20px;
+ overflow: hidden;
+ background: var(--wpds-color-bg-surface-neutral);
+ /* New stacking context so the dot grid's negative z-index stays behind
+ this page's content without escaping below ancestors. */
+ isolation: isolate;
+}
+
+/* Takes whatever height the footer doesn't need, and centres within it. */
+.contentArea {
+ flex: 1 1 auto;
+ min-height: 0;
display: flex;
flex-direction: column;
- align-items: center;
justify-content: center;
- min-height: 100vh;
- padding: 48px 32px;
+ align-items: center;
+}
+
+.footer {
+ /* Positioned for the same reason as .analyticsPreference: the content
+ scrim's backdrop-filter would otherwise blur the footer along with the
+ dot grid behind it. */
+ position: relative;
+ z-index: 1;
+ flex: 0 0 auto;
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 24px;
+ padding-top: 16px;
+}
+
+/* Decorative dot-grid backdrop, same treatment as the onboarding shell. */
+.pageDotGrid {
+ position: fixed;
+ inset: 0;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: -1;
+}
+
+/* Window drag strips along the chromeless edges — same pattern as
+ onboarding-layout. Interactive elements overlapping them stay clickable
+ via the global no-drag rule. */
+.dragEdge {
+ position: fixed;
+ pointer-events: none;
+ -webkit-app-region: drag;
+}
+
+.dragEdgeTop {
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 36px;
+}
+
+.dragEdgeLeft {
+ top: 36px;
+ left: 0;
+ bottom: 0;
+ width: 16px;
+}
+
+.dragEdgeBottom {
+ left: 16px;
+ right: 0;
+ bottom: 0;
+ height: 16px;
+}
+
+.skipButton {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
text-align: center;
- box-sizing: border-box;
+ max-width: 640px;
+ text-wrap: pretty;
}
-.heading {
- font-size: var(--wpds-typography-font-size-2xl);
- font-weight: var(--wpds-typography-font-weight-regular);
- margin: 0 0 8px;
+/* Layout stage for the toy: sized for the 380px logo itself; the canvas is
+ wider/taller (logo + particle padding) and spills past this box without
+ clipping (contained={false}), so displaced pixels roam freely. */
+.logoToy {
+ position: relative;
+ width: 780px;
+ height: 420px;
}
-.subtitle {
- margin: 0 0 32px;
- color: var(--wpds-color-fg-content-neutral-weak);
- font-size: var(--wpds-typography-font-size-md);
- max-width: 400px;
+/* The toy reacts to hover/click but isn't a control — don't advertise it
+ with a pointer cursor (the shared canvas style sets one). */
+.logoToy canvas {
+ cursor: default;
}
-.features {
- list-style: none;
- margin: 0 0 32px;
- padding: 0;
+/* Text block pulled up over the lower part of the toy. Decorative copy:
+ not selectable, and transparent to the pointer so hovering "through" it
+ still drives the toy's particles underneath. */
+.contentBody {
+ position: relative;
display: flex;
flex-direction: column;
+ align-items: center;
gap: 12px;
- text-align: left;
- max-width: 360px;
+ margin-top: -140px;
+ padding-top: 40px;
+ pointer-events: none;
+ user-select: none;
}
-.features li {
- font-size: var(--wpds-typography-font-size-sm);
- color: var(--wpds-color-fg-content-neutral-weak);
- padding-left: 24px;
+/* The absolutely-positioned scrim would otherwise paint above its static
+ siblings; positioning them keeps the text on top (later DOM order). */
+.contentBody > :not(.contentScrim) {
position: relative;
}
-.features li::before {
- content: '✓';
+/* Progressive blur behind the overlapping text — same pattern as the
+ onboarding footer scrim: the backdrop blur fades in from the top so the
+ toy's particles soften under the text without a hard edge.
+ Oversized on purpose: it reaches past the viewport on the sides and
+ bottom and lets .page's overflow clipping trim it, so the blur covers
+ everything below the title (text, footer, buttons) while the dot grid
+ behind it stays soft. */
+.contentScrim {
position: absolute;
- left: 0;
- color: var(--wpds-color-fg-content-neutral);
+ top: -56px;
+ left: -100vw;
+ right: -100vw;
+ bottom: -100vh;
+ pointer-events: none;
+ backdrop-filter: blur(12px);
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ color-mix(in srgb, var(--wpds-color-bg-surface-neutral) 55%, transparent) 96px
+ );
+ -webkit-mask-image: linear-gradient(to bottom, transparent, black 96px);
+ mask-image: linear-gradient(to bottom, transparent, black 96px);
+}
+
+.title {
+ margin: 0;
+ text-wrap: balance;
+}
+
+.subtitle {
+ color: var(--wpds-color-fg-content-neutral-weak);
+ max-width: 440px;
+ margin: 0;
+}
+
+/* Bottom-left, opposite the action buttons — one footer row with them. */
+.legal {
+ max-width: 340px;
+ margin: 0;
+ color: var(--wpds-color-fg-content-neutral-weak);
+ font-size: 12px;
+ line-height: 20px;
+ text-wrap: pretty;
+}
+
+/* Same treatment as the shared learn-more link (a button that opens an
+ external URL through the connector). */
+.linkButton {
+ background: none;
+ border: none;
+ padding: 0;
+ font: inherit;
+ color: var(--wpds-color-fg-interactive-brand);
+ text-decoration: underline;
+ cursor: pointer;
+ white-space: nowrap;
}
-.actions {
+.linkButton:hover,
+.linkButton:focus-visible {
+ text-decoration: none;
+}
+
+.linkButton:disabled {
+ color: var(--wpds-color-fg-interactive-neutral-disabled);
+ cursor: default;
+}
+
+.footerActions {
display: flex;
- flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.footerActions > * {
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+
+/* Sits just below the benefits card, centered in the content column.
+ Positioned so it paints above the content scrim (a positioned element
+ with backdrop-filter) instead of being washed out behind it. */
+.analyticsPreference {
+ position: relative;
+ z-index: 1;
+ flex: 0 0 auto;
+ margin-top: 20px;
+ color: var(--wpds-color-fg-content-neutral-weak);
+ font-size: var(--wpds-typography-font-size-sm);
+}
+
+/* Signed-in identity shown in place of the auth buttons. */
+.identity {
+ display: inline-flex;
+ align-items: center;
gap: 8px;
- width: 280px;
+ font-size: 13px;
+ font-weight: var(--wpds-typography-font-weight-medium);
}
-.skip {
- margin-top: 16px;
+.identityAvatar {
+ width: 24px;
+ height: 24px;
}
-.footer {
- margin-top: 32px;
- display: flex;
- justify-content: center;
+/* The toy is the only part with slack, so it absorbs the loss as the window
+ gets shorter (down to the app's 600px minimum). The canvas is sized by
+ `min(100%, …)`, so shrinking the stage scales it down; the text block's
+ overlap eases off with it. */
+@media (max-height: 800px) {
+ .logoToy {
+ width: 620px;
+ height: 320px;
+ }
+
+ .contentBody {
+ margin-top: -110px;
+ }
+}
+
+@media (max-height: 700px) {
+ .logoToy {
+ width: 480px;
+ height: 230px;
+ }
+
+ .contentBody {
+ margin-top: -80px;
+ padding-top: 28px;
+ }
+
+ .contentScrim {
+ top: -40px;
+ }
}
diff --git a/apps/ui/src/ui-classic/router/router.tsx b/apps/ui/src/ui-classic/router/router.tsx
index 5f73ce531f..7564887bfe 100644
--- a/apps/ui/src/ui-classic/router/router.tsx
+++ b/apps/ui/src/ui-classic/router/router.tsx
@@ -11,6 +11,7 @@ import { onboardingConnectRoute } from './route-onboarding-connect';
import { onboardingCreateRoute } from './route-onboarding-create';
import { onboardingHomeRoute } from './route-onboarding-home';
import { onboardingImportRoute } from './route-onboarding-import';
+import { onboardingTourRoute } from './route-onboarding-tour';
import { sessionDetailRoute } from './route-session-detail';
import { settingsRoute } from './route-settings';
import { siteOverviewRoute } from './route-site-overview';
@@ -30,6 +31,7 @@ const routeTree = rootRoute.addChildren( [
] ),
onboardingLayoutRoute.addChildren( [
onboardingHomeRoute,
+ onboardingTourRoute,
onboardingConnectRoute,
onboardingCreateRoute,
onboardingBlueprintRoute,