diff --git a/docs/app/(home)/cloud/CloudCodeBlock.module.css b/docs/app/(home)/cloud/CloudCodeBlock.module.css index f0570a63fe..506725d05f 100644 --- a/docs/app/(home)/cloud/CloudCodeBlock.module.css +++ b/docs/app/(home)/cloud/CloudCodeBlock.module.css @@ -47,7 +47,20 @@ } @media (max-width: 767px) { + /* Opt-in, off by default. A host that sets this renders the panel at its + desktop measure and scales the whole box down: zoom shrinks the layout box + too, so a percentage width resolves in the panel's own scaled space and the + panel keeps the proportions it has on a wide screen instead of clipping + lines at phone width. */ + .panel { + zoom: var(--cloud-code-mobile-zoom, 1); + } + .code { + /* Opt-in, off by default: a host that zooms the panel down can also pull the + code's own padding in, which the zoom would otherwise leave looking + generous relative to the shrunken type. */ + padding-block: var(--cloud-code-mobile-padding-block, 48px); scrollbar-width: none; } diff --git a/docs/app/(home)/cloud/CloudIntegrationSection.module.css b/docs/app/(home)/cloud/CloudIntegrationSection.module.css index 6fc659e939..08e49f84aa 100644 --- a/docs/app/(home)/cloud/CloudIntegrationSection.module.css +++ b/docs/app/(home)/cloud/CloudIntegrationSection.module.css @@ -319,10 +319,28 @@ width: calc(100% - 32px); } + /* A host that sets this to `contents` drops .content as a box, so its two + children join .inner's column and the orders below can put the code between + them. Left as `flex` the orders are inert: .intro and .stepper are ordered + inside .content in the sequence they already appear in, and .codeColumn is + the second of .inner's two items either way. */ .content { + display: var(--cloud-setup-content-display, flex); gap: 32px; } + .intro { + order: 1; + } + + .codeColumn { + order: 2; + } + + .stepper { + order: 3; + } + .stepper[data-interactive="true"][data-step-count="2"] { height: 248px; } diff --git a/docs/app/(home)/cloud/observability/CloudCtaSection.module.css b/docs/app/(home)/cloud/observability/CloudCtaSection.module.css index 9789715982..a6d0425fff 100644 --- a/docs/app/(home)/cloud/observability/CloudCtaSection.module.css +++ b/docs/app/(home)/cloud/observability/CloudCtaSection.module.css @@ -1,52 +1,57 @@ +/* No surface of its own: the CTA sits straight on the page. The FAQ band below + brings its own top gap, so this only tops that up rather than stating the + full measure again. */ .section { width: 100%; - padding: var(--observability-section-gap, 160px) var(--home-section-padding-inline) 0; - background: var(--openui-foreground); + padding: var(--observability-section-gap, 160px) var(--home-section-padding-inline) + calc(var(--observability-section-gap, 160px) / 2); } +/* One centred column, read top to bottom: the ask, the field that answers it, + then the terms. A two-column split put the title and the field on opposite + sides of 1200px, with the eye crossing the page between reading and acting. */ .inner { display: flex; + flex-direction: column; align-items: center; - justify-content: space-between; - gap: 48px; + gap: 32px; width: 100%; max-width: 1200px; margin-inline: auto; + text-align: center; } .copy { - flex: 1 1 auto; + display: flex; + flex-direction: column; + align-items: center; min-width: 0; } -.note { - margin: 0.75rem 0 0; - font-size: 1.0625rem; - line-height: 1.5; - color: var(--openui-text-neutral-secondary); -} - -.pricingLink { - color: var(--openui-text-neutral-primary); - text-decoration: none; - border-bottom: 1px solid currentColor; - padding-bottom: 1px; -} +/* The offer line above the title. inline-flex rather than a run of text, so the + mark sits on the line's centre instead of its baseline; the gap replaces the + space flex layout drops between the children. -.pricingLink:hover { - opacity: 0.7; -} - -.accessNote { - margin: 0; - color: var(--openui-text-neutral-secondary); + Green is the same #049156 the trace diagram's pass marks and the hero's own + offer line use. Centred by the column, so it needs no alignment of its own. */ +.offer { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0 0 12px; + color: #049156; font-size: 0.875rem; + font-weight: 600; line-height: 1.5; } -.accessNote strong { - color: var(--openui-text-neutral-primary); - font-weight: 600; +[data-theme="dark"] .offer { + /* The solid reads as near-black on the dark ground, so the line lifts. */ + color: #3ecf8e; +} + +.offerIcon { + flex: none; } .title { @@ -66,45 +71,13 @@ display: flex; flex: 0 0 auto; flex-direction: column; - align-items: flex-start; - gap: 12px; -} - -.formTitle { - margin: 0; - color: var(--openui-text-neutral-primary); - font-family: var(--home-font-display); - font-size: var(--home-heading-size); - font-weight: var(--home-heading-weight); - line-height: var(--home-heading-leading); - letter-spacing: var(--home-heading-tracking); -} - -.separator { - width: 100%; - max-width: 1200px; - height: 1px; - margin: var(--observability-section-gap, 160px) auto 0; - background: var(--home-hairline); -} - -@media (max-width: 1023px) { - .inner { - flex-direction: column; - align-items: flex-start; - gap: 32px; - } + align-items: center; + /* The only gap in here, between the field and the link below it. */ + gap: 20px; } @media (max-width: 767px) { - .inner { - align-items: center; - text-align: center; - } - .actions { - flex-direction: column; - align-items: center; width: 100%; max-width: 320px; } diff --git a/docs/app/(home)/cloud/observability/CloudCtaSection.tsx b/docs/app/(home)/cloud/observability/CloudCtaSection.tsx index 6c17f11855..ad5b9c435f 100644 --- a/docs/app/(home)/cloud/observability/CloudCtaSection.tsx +++ b/docs/app/(home)/cloud/observability/CloudCtaSection.tsx @@ -1,3 +1,6 @@ +/* ssr entry: this section is a server component. */ +import { Button } from "@/components/button"; +import { Gift } from "@phosphor-icons/react/dist/ssr"; import styles from "./CloudCtaSection.module.css"; import { EarlyAccessForm } from "./EarlyAccessForm"; @@ -6,25 +9,24 @@ export function CloudCtaSection() {
+ {/* Above the title rather than under the field: the offer is the + reason to read the line below it, not a footnote to the form. */} +

+

See your agent through your users’ eyes

-

See what works, what’s missing, and what to build next.

{/* The same capture as the hero, so the page opens and closes on the one action. */}
-

Join waitlist

-

- Free during early access.{" "} - - View pricing - -

+ {/* The same tertiary button the trust section uses for its own link. */} +
-
); } diff --git a/docs/app/(home)/cloud/observability/FaqSection.tsx b/docs/app/(home)/cloud/observability/FaqSection.tsx index 16e74cce1e..fc39d35bc7 100644 --- a/docs/app/(home)/cloud/observability/FaqSection.tsx +++ b/docs/app/(home)/cloud/observability/FaqSection.tsx @@ -54,7 +54,9 @@ export function FaqSection() { firstOpen contact={ <> - Have another question? Join our{" "} + Have another question? +
+ Join our{" "} * { + width: 100%; + height: 100%; +} + .featureImage { + position: relative; + z-index: 1; display: block; + width: 100%; + height: 100%; object-fit: cover; } @@ -140,10 +166,9 @@ gap: 32px; } - .featureImage { + .shot { flex: 0 0 auto; width: 100%; - height: auto; aspect-ratio: 720 / 400; } @@ -167,26 +192,40 @@ width: calc(100% - 32px); } - .featureImage { - order: -1; - aspect-ratio: 4 / 3; + /* One gap governs both sides of the shot, so heading-to-image and + image-to-description stay equal by construction. */ + .card { + gap: 20px; + } + + /* The artwork's own ratio, the same as desktop. A 4/3 stage cut roughly a + quarter of the width off every 1.8 screenshot, straight through the UI, and + the per-image object-position offsets below it were chasing that crop + rather than removing it. */ + .shot { + aspect-ratio: 720 / 400; border-radius: 12px; - object-fit: cover; } - .featureImageTriage { - /* The review drawer is the triage story; keep it readable rather than - shrinking the entire 1440px console into a phone-width slot. */ - object-position: 72% center; + /* Name the feature, show it, then explain it. display:contents drops .content + as a box so its two halves join the card's column and the shot can be + ordered between them. The orders are absolute rather than relative, so + every card reads the same way whichever side its art takes on desktop. */ + .content { + display: contents; } - .featureImageEvals { - /* Keeps the selected case and its run history in the crop. */ - object-position: 58% center; + .headingGroup { + order: 1; } - .content { - gap: 20px; + .shot, + .shotPlaceholder { + order: 2; + } + + .bodyGroup { + order: 3; } } diff --git a/docs/app/(home)/cloud/observability/FeaturesSection.tsx b/docs/app/(home)/cloud/observability/FeaturesSection.tsx index 8f456ad3d8..0ad4253e77 100644 --- a/docs/app/(home)/cloud/observability/FeaturesSection.tsx +++ b/docs/app/(home)/cloud/observability/FeaturesSection.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import type { ReactNode } from "react"; +import { FadedDither } from "../../components/FadedDither/FadedDither"; import { SectionHeader } from "../../components/SectionHeader/SectionHeader"; import styles from "./FeaturesSection.module.css"; @@ -54,7 +55,12 @@ function FeatureShot({ shot, alt }: { shot?: string; alt: string }) { const imageHeight = shot === "session-replay" ? 1804 : 1600; return ( - <> + /* Shader behind, artwork on top, the same three-layer stage the home page's + product bands use. The shots carry a transparent margin, so the texture + reads around them. band="light" because these cards sit straight on the + page, which is white on a light theme and black on a dark one. */ +
+ - +
); } @@ -90,20 +96,21 @@ type Feature = { }; /* One loop, in order: see it, find the ones worth seeing, mark what is wrong, - stop it recurring. Read as four steps rather than four capabilities, so each - one hands to the next and the last leaves the product entirely. + stop it recurring, then decide what to build next. Read as steps rather than + capabilities, so each one hands to the next. - Timeline folded into step one — following the journey is part of seeing what - the user saw, not a separate screen. Insights folded into step two as a single - clause: the demand signal comes from the same view you triage in, and giving - it its own step would break the loop. */ + Timeline is folded into step one, since following the journey is part of + seeing what the user saw rather than a separate screen. Insights used to be + folded into Triage on the same reasoning, but the two answer different + questions: Triage is which session to open, Insights is what recurs across + all of them, so it closes the loop instead of sitting inside step two. */ const FEATURES: Feature[] = [ { title: "Session replay", shot: "session-replay", headline: ( <> - See every + Understand every
user session @@ -115,9 +122,9 @@ const FEATURES: Feature[] = [ shot: "triage-figma", headline: ( <> - Find sessions + Discover sessions
- worth opening + worth investigating ), description: @@ -147,6 +154,18 @@ const FEATURES: Feature[] = [ ), description: "Turn production failures into evals that catch regressions before release.", }, + { + title: "Insights", + shot: "insights", + headline: ( + <> + Discover what to +
+ build next + + ), + description: "Aggregate demand across every session to see which needs recur and how often.", + }, ]; export function FeaturesSection() { @@ -156,7 +175,7 @@ export function FeaturesSection() { Connect each response to what users saw,
diff --git a/docs/app/(home)/cloud/observability/TraceGapDiagram.module.css b/docs/app/(home)/cloud/observability/TraceGapDiagram.module.css index 35055d220e..a5b8cb39e6 100644 --- a/docs/app/(home)/cloud/observability/TraceGapDiagram.module.css +++ b/docs/app/(home)/cloud/observability/TraceGapDiagram.module.css @@ -1,348 +1,212 @@ +/* One card split down the middle, rather than two cards with a gap between + them: the two sides are halves of a single argument, and a shared edge says + so where two separate cards said they were separate things. Plain text + inside, since the contrast between the halves is the whole diagram. */ .diagram { - --trace-surface: color-mix(in srgb, var(--openui-text-neutral-primary) 4%, transparent); - --trace-surface-strong: color-mix(in srgb, var(--openui-text-neutral-primary) 6%, transparent); - --trace-border: color-mix(in srgb, var(--openui-text-neutral-primary) 10%, transparent); + /* Light: a white card lifted off the page by an edge and a shadow. Dark + inverts to a tint of the ground with neither, since a shadow on black shows + nothing and an edge there only fences the card in. */ + --trace-surface: var(--openui-foreground); + --trace-stroke: rgba(10, 10, 10, 0.08); + --trace-shadow: var(--home-shadow-card); + /* The split between the halves. Lighter than the row rules inside each half: + it separates two columns that already read as separate, so it does not need + to announce itself. */ + --trace-divider: color-mix(in srgb, var(--openui-text-neutral-primary) 5%, transparent); - margin: 0; - color: var(--openui-text-neutral-primary); - font-family: var(--home-font-text); - font-size: var(--home-body-size); - line-height: var(--home-body-leading); -} - -.conversation { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 40px; - margin: 0; - padding: 0; - list-style: none; -} - -.message { - position: relative; - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} - -.avatar { - display: flex; - flex-shrink: 0; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + align-items: stretch; + /* The halves square off against the rounded corners, so the card clips them. */ overflow: hidden; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - border: 1px solid var(--trace-border); - background: var(--trace-surface); -} - -.avatar img { - object-fit: cover; -} - -.agentAvatar { - border: 1px solid var(--openui-border-default); - background: var(--home-surface-ink); -} - -.bubble { - flex: 1; - min-width: 0; - min-height: 56px; - box-sizing: border-box; margin: 0; - padding: 14px 18px; - border: 1px solid var(--trace-border); - border-radius: 16px; + border: 1px solid var(--trace-stroke); + border-radius: 20px; background: var(--trace-surface); + box-shadow: var(--trace-shadow); + color: var(--openui-text-neutral-primary); + font-family: var(--home-font-text); } -.bubble strong { - font-weight: 600; -} - -.agentBubble { - background: var(--trace-surface-strong); -} - -.arrow { - position: absolute; - top: 50%; - right: -35px; - color: var(--openui-text-neutral-secondary); - transform: translateY(-50%); -} - -/* The same response is read two ways: technical completion on the left, - the unresolved user outcome on the right. The status badges make the - contradiction legible before the viewer reads any row copy. */ -.comparison { - display: grid; - grid-template-columns: minmax(0, 1fr) 64px minmax(0, 1fr); - align-items: stretch; - gap: 12px; - width: 100%; - margin-top: 56px; +[data-theme="dark"] .diagram { + --trace-surface: rgba(255, 255, 255, 0.04); + --trace-stroke: transparent; + --trace-shadow: none; } -.panel { - position: relative; +.half { display: flex; min-width: 0; flex-direction: column; - gap: 24px; - padding: 24px 24px 28px; - border: 1px solid var(--trace-border); - border-radius: 20px; - background: var(--trace-surface); -} - -.outcomePanel { - border-color: var(--trace-border); - background: #fff; + /* Even on all four sides. The last row carries its own 14px below the text, + so the bottom reads a little deeper than the top, which suits a block that + ends on a list. */ + padding: 32px; } -:global(.dark) .outcomePanel, -:global([data-theme="dark"]) .outcomePanel { - background: var(--trace-surface); +/* The split. Declared on the second half rather than as a track of its own, so + it cannot fall out of step with the grid. */ +.half + .half { + border-left: 1px solid var(--trace-divider); } -.panelHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 20px; - min-height: 64px; -} - -.panelHeader > div { - min-width: 0; -} - -.panelLabel { - display: block; - margin-bottom: 8px; +/* Inherits the diagram's text face rather than setting mono of its own, so the + label reads as the same voice as the lines under it. */ +.eyebrow { + margin: 0 0 1rem; color: var(--openui-text-neutral-secondary); - font-family: var(--home-font-mono); font-size: var(--home-label-size); font-weight: var(--home-label-weight); line-height: 1; - letter-spacing: 0.04em; - text-transform: uppercase; } -.panelHeader h3 { - max-width: 18ch; - margin: 0; +.heading { + /* Enough that the rule below reads as the top of the list rather than an + underline on the gloss line, and the eyebrow above reads as a label on the + half rather than a third line of the title. */ + margin: 0 0 1.75rem; font-family: var(--home-font-display); - font-size: var(--home-heading-size); - font-weight: var(--home-heading-weight); + /* Set locally rather than from --home-heading-*, which is 22px/500 here and + drops to 16px/400 on a phone. This heading is the diagram's own title, not + a card heading inside a list of them, so it carries the larger size at the + display face's usual weight. */ + font-size: clamp(1.125rem, 2.2vw, 1.625rem); + font-weight: 500; line-height: var(--home-heading-leading); letter-spacing: var(--home-heading-tracking); - text-wrap: balance; -} - -.resultBadge { - display: inline-flex; - flex: none; - align-items: center; - min-height: 28px; - padding: 5px 10px; - border-radius: 999px; - color: #fff; - font-family: var(--home-font-text); - font-size: var(--home-label-size); - font-weight: 500; - line-height: 1; - white-space: nowrap; } -.passedBadge { - background: #049156; -} - -.failedBadge { - background: #b91c1c; +/* The heading's second line, set like the page's own title/subtitle pair: same + size and weight, dropped to the secondary colour. */ +.headingGloss { + display: block; + color: var(--openui-text-neutral-secondary); } -.checklist { +/* Equal rows over whatever height the taller half settles at, so a line in one + stays level with the line it answers in the other. The halves are grid items + on a stretch-aligned row, so they share that height already. */ +.list { display: grid; - grid-template-rows: repeat(3, minmax(min-content, 1fr)); + grid-template-rows: repeat(4, minmax(min-content, 1fr)); flex: 1; - gap: 0; margin: 0; padding: 0; list-style: none; } -.checklist li { +.list li { display: flex; align-items: center; - gap: 16px; + gap: 12px; min-width: 0; padding-block: 14px; - border-top: 1px solid var(--home-hairline); -} - -.checkCopy { - flex: 1; - min-width: 0; -} - -.checkCopy > span { - display: block; - margin-bottom: 3px; - font-family: var(--home-font-display); - font-weight: var(--home-heading-weight); -} - -.checkCopy p { - margin: 0; - color: var(--openui-text-neutral-secondary); -} - -.statusIcon { - flex-shrink: 0; - width: 24px; - height: 24px; -} - -.failure { - color: var(--openui-text-danger-primary); - border-color: color-mix(in srgb, #b91c1c 20%, transparent); - background: color-mix(in srgb, #b91c1c 6%, var(--trace-surface)); -} - -.notEqual { - position: relative; - display: flex; + /* A gradient rather than a border, so the rule can fade out at both ends. + Painted at the top of the row, 1px tall, full width. */ + background-image: linear-gradient( + to right, + transparent, + var(--home-hairline) 8%, + var(--home-hairline) 92%, + transparent + ); + background-repeat: no-repeat; + background-position: top; + background-size: 100% 1px; + /* The same tokens the section lead uses (gateway/sections.module.css .lead), + so the rows read at the size of the paragraph that introduces them and move + with it if that scale is ever retuned. */ + font-size: var(--home-lead-size); + line-height: var(--home-lead-leading); +} + +/* Both lists are set identically. The marks carry the contrast, so weighting + one side as well would say the same thing twice. */ +.mark { + display: inline-flex; + flex: none; align-items: center; justify-content: center; - color: var(--openui-text-neutral-primary); -} - -.notEqual::before, -.notEqual::after { - position: absolute; - left: 50%; - width: 1px; - height: calc(50% - 34px); - background: var(--home-hairline); - content: ""; -} - -.notEqual::before { - top: 0; + width: 22px; + height: 22px; + /* One shape for both sides; only the colour separates them. */ + border-radius: 7px; + color: #fff; } -.notEqual::after { - bottom: 0; +.markPass { + background: #049156; } -.notEqual svg { - position: relative; - z-index: 1; - box-sizing: content-box; - padding: 12px; - border: 1px solid var(--trace-border); - border-radius: 50%; - background: var(--openui-background); +.markFail { + background: #b91c1c; } @media (max-width: 1023px) { - .conversation { - gap: 32px; - } - - .bubble { - padding: 12px 16px; - } - - .arrow { - right: -27px; - } - - .comparison { - grid-template-columns: minmax(0, 1fr) 48px minmax(0, 1fr); - gap: 10px; - } - - .panel { - padding: 24px 20px 28px; - } - - .panelHeader { - flex-direction: column; - gap: 12px; + .half { + padding: 28px 24px; } } @media (max-width: 767px) { - .conversation { + /* Stacked: two halves of wrapping phrases side by side on a phone leaves each + line three or four words deep. The pairing gives way to legibility here, + which is why each half keeps a heading that stands on its own. */ + .diagram { grid-template-columns: minmax(0, 1fr); - gap: 32px; } - .arrow { - top: auto; - right: auto; - bottom: -27px; - left: calc(50% - 10px); - transform: rotate(90deg); + /* The split turns with the layout. */ + .half + .half { + border-top: 1px solid var(--trace-divider); + border-left: none; } - .comparison { - grid-template-columns: minmax(0, 1fr); - gap: 16px; - margin-top: 48px; - } - - .panel { - gap: 24px; - padding: 20px; + /* Stacked, the card runs to ~830px on a phone, which is more than a screen. + The height is in the padding rather than the type: each row is 24px of text + inside 32px of padding, so tightening the spacing buys far more than + shrinking the words would, and costs nothing in legibility. */ + .half { + padding: 16px; } - .panelHeader { - min-height: 0; + .eyebrow { + margin-bottom: 10px; } - .notEqual { - height: 52px; + .heading { + margin-bottom: 16px; + font-size: 1.0625rem; } - .notEqual::before, - .notEqual::after { - top: 50%; - width: calc(50% - 34px); - height: 1px; - } - - .notEqual::before { - right: auto; - left: 0; - } - - .notEqual::after { - right: 0; - left: auto; + .list { + display: flex; + flex-direction: column; } - .notEqual svg { - width: 24px; - height: 24px; - padding: 10px; + .list li { + align-items: flex-start; + padding-block: 10px; + /* A step under --home-lead-size, which is 1rem here. The rows are short + statements rather than running prose, so they take the smaller measure + without becoming harder to scan. */ + font-size: 0.9375rem; } - .checklist { - display: flex; - flex-direction: column; + /* Smaller tiles to match the tighter rows. Wrapping lines also push the mark + off the first one once it is centred, so it sits on the first line's + optical centre instead. */ + .mark { + width: 18px; + height: 18px; + margin-top: 2px; + border-radius: 6px; } - .checklist li { - padding-block: 16px; + /* The glyph's size prop is set once in the component, so the breakpoint + resizes the rendered SVG rather than the prop. */ + .mark svg { + width: 11px; + height: 11px; } } diff --git a/docs/app/(home)/cloud/observability/TraceGapDiagram.tsx b/docs/app/(home)/cloud/observability/TraceGapDiagram.tsx index d382770cb6..77bf4c3c5d 100644 --- a/docs/app/(home)/cloud/observability/TraceGapDiagram.tsx +++ b/docs/app/(home)/cloud/observability/TraceGapDiagram.tsx @@ -1,120 +1,64 @@ -import { ArrowRight, EqualNot } from "lucide-react"; -import Image from "next/image"; -import agentLogo from "./assets/trace-agent.svg"; -import failedStatus from "./assets/trace-fail.svg"; -import passedStatus from "./assets/trace-pass.svg"; -import userAvatar from "./assets/trace-user.png"; +/* ssr entry: this stays a server component, so the icons cannot come from the + main entry, which ships client code. */ +import { Check, X } from "@phosphor-icons/react/dist/ssr"; import styles from "./TraceGapDiagram.module.css"; -const TRACE_STEPS = [ - { title: "Tool calls succeeded", description: "Every call completed" }, - { title: "The model responded", description: "No generation errors" }, - { title: "The request completed", description: "Response returned 200" }, -]; +/* Kept as pairs rather than two independent lists: each line sits opposite the + one it answers, and the cards give their rows equal heights so the pairing + survives the split. Editing one side means editing its partner. -const BLIND_SPOTS = [ - { - title: "Did the user get what they needed?", - description: "The trace looks successful even when the answer is useless", - }, - { - title: "What did the user actually experience?", - description: "It records the response, not the UI they saw or how they used it", - }, - { - title: "What should we build next?", - description: "Errors and latency don’t show what users tried to do but couldn’t", - }, + The broadest pair, what the agent did against what the user experienced, is + not in here: it sits in the two headings as their second line, where it + states the contrast the rest of the rows then work through. */ +const ROWS = [ + { trace: "The request succeeded", outcome: "Did the user get what they needed?" }, + { trace: "No errors occurred", outcome: "Did the experience actually work?" }, + { trace: "The model and tools worked", outcome: "Why did the user retry or leave?" }, + { trace: "The system behaved as expected", outcome: "What needs to improve for the user?" }, ]; -function StatusMark({ failed = false }: { failed?: boolean }) { - return ( - {failed - ); -} - export function TraceGapDiagram() { return (
-
    -
  1. - -

    - Compare revenue by region. -

    -
  2. -
  3. -
    - OpenUI -
    -

    Revenue is up 17% this month.

    -
  4. -
  5. - -

    - But which region is driving it? -

    -
  6. -
-
-
-
-
- Execution -

Tracing says everything worked

-
- All checks passed -
-
    - {TRACE_STEPS.map((step) => ( -
  1. -
    - {step.title} -

    {step.description}

    -
    - -
  2. - ))} -
-
-
-
-
-
-
- User outcome -

But tracing can’t tell you…

-
- Not captured -
-
    - {BLIND_SPOTS.map((point) => ( -
  • -
    - {point.title} -

    {point.description}

    -
    - -
  • - ))} -
-
+
+

Execution : Successful

+

+ Traces can tell you + What the agent did +

+
    + {ROWS.map(({ trace }) => ( +
  • + {/* The heading already says which side of the argument this is, so + the mark repeats it for the eye only. */} + + {trace} +
  • + ))} +
+
+ +
+

Outcome : Failed

+

+ But traces can’t tell you + What the user experienced +

+
    + {ROWS.map(({ outcome }) => ( +
  • + + {outcome} +
  • + ))} +
); diff --git a/docs/app/(home)/cloud/observability/page.module.css b/docs/app/(home)/cloud/observability/page.module.css index dbcaa7c9e2..45e477785b 100644 --- a/docs/app/(home)/cloud/observability/page.module.css +++ b/docs/app/(home)/cloud/observability/page.module.css @@ -75,6 +75,32 @@ color: rgba(255, 255, 255, 0.55); } +/* A second line under the subtitle. Green, because it is the one line here that + is an offer rather than a description, and #049156 is the green already on + this page in the trace diagram's pass marks. */ +/* A block, so it takes its own line and inherits the lockup's text-align: the + desktop hero sets it left, the mobile one centre. The flex box lives on the + inner span, which is inline-level and so gets positioned by that alignment. + One element doing both would pin the line to the left at every width. */ +.subtitleTerms { + display: block; + margin-top: 12px; + /* No size of its own: it reads at the subtitle's measure, as a second line of + the same block rather than a footnote to it. */ + color: #049156; +} + +.subtitleTermsInner { + display: inline-flex; + align-items: center; + gap: 8px; +} + +[data-theme="dark"] .subtitleTerms { + /* The solid reads as near-black on the dark ground, so the line lifts. */ + color: #3ecf8e; +} + .subtitle { display: block; flex: 0 0 400px; @@ -98,6 +124,8 @@ } .heroImage { + position: relative; + z-index: 1; display: block; width: 100%; height: auto; @@ -109,16 +137,37 @@ --observability-section-gap: 160px; } +/* Grey, like the trust section that follows it: the two read as one band of + practical detail after the feature flow. */ .integrationSection { width: 100%; padding-block: var(--observability-section-gap); - /* Setup stays on the same surface as the feature flow and owns equal space - above and below its content. */ - background: var(--openui-foreground); + background: var(--openui-background); + /* The snippet is nine lines and measures 343px with the code block's own + padding. The component defaults to 420px, which left ~80px of empty panel + below the last line. The phone override below still wins at that width. */ + --cloud-code-min-height: 344px; } +/* Scoped to this page. EnterpriseSection is shared with /cloud and + /cloud/gateway, which keep its own 160px and its own grey. + + The class is doubled to lift it to (0,2,0). Its own .section rule sets both + of these at (0,1,0), and which of two equally specific rules wins comes down + to the order the two CSS modules happen to load in, which is not something + to rely on. */ +.trustSection.trustSection { + padding-block: clamp(4rem, 7vw, 6rem); + /* Sits on the page rather than in a band: the grey now belongs to the setup + block below, which is the one thing on the page that is a detour. */ + background: transparent; +} + +/* Less below than above: the footer follows, and the shared gap on both sides + left the page ending on a long empty stretch. */ .faqBand { - padding: var(--observability-section-gap) var(--home-section-padding-inline); + padding: var(--observability-section-gap) var(--home-section-padding-inline) + calc(var(--observability-section-gap) / 2); } .faqBand > section { @@ -139,7 +188,18 @@ @media (max-width: 767px) { .integrationSection { - --cloud-code-min-height: 240px; + /* Keeps the panel's desktop measure and scales the whole thing down, so the + snippet reads at the same proportions rather than losing its right-hand + half to the clip. The desktop --cloud-code-min-height above rides along + in the same scaled space, which is what holds the ratio. */ + --cloud-code-mobile-zoom: 0.54; + --cloud-code-mobile-padding-block: 28px; + /* Content-sized, so the panel hugs the snippet. Left at the desktop 344px + the shorter padding above would be matched by 40px of empty panel below, + which is the one thing equal padding cannot survive. */ + --cloud-code-min-height: 0; + /* The steps explain the snippet, so they follow it here. */ + --cloud-setup-content-display: contents; } .integrationSection > div { @@ -276,25 +336,62 @@ opacity: 0.6; } -/* The hero art dissolves into the page at its lower edge instead of stopping on - a straight line. Masked rather than overlaid with a gradient, so it works on - either theme without matching a background colour. */ -.heroImage { +/* The hero sits on a stage now, so it ends on its own edge rather than + dissolving into the page. The stroke is the shader's base colour, so the edge + matches the fill inside it. Keep in step with TONES in FadedDitherCanvas.tsx + and with .shot in FeaturesSection.module.css. */ +.heroStage { + --hero-stroke: #f8f8f8; + + position: relative; + width: 100%; + overflow: hidden; + border: 1px solid var(--hero-stroke); + border-radius: 24px; +} + +[data-theme="dark"] .heroStage { + --hero-stroke: #0e0e0e; +} + +/* The mobile hero keeps the old treatment: no stage behind it, so it dissolves + into the page at its lower edge rather than ending on a straight line. + Masked rather than overlaid with a gradient, so it works on either theme + without having to match a background colour. */ +.heroImageMobile { -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%); mask-image: linear-gradient(to bottom, #000 62%, transparent 100%); } +.heroShader { + position: absolute; + z-index: 0; + inset: 0; +} + +.heroShader > * { + width: 100%; + height: 100%; +} + /* Keep the action persistent: the waitlist should never depend on a hover state to explain what the arrow does. */ .emailSubmit { flex: none; display: inline-flex; align-items: center; - justify-content: center; + /* Not centred: min-width leaves a few pixels of slack that centring splits + between the two ends, which pushed the badge ~13px off the right edge while + the padding said 8. Pinning each end to its own padding makes the space + around the badge match the 8px above and below it. */ + justify-content: space-between; min-width: 10.25rem; height: 3rem; gap: 10px; - padding: 0 8px 0 20px; + /* 7, not 8: the badge is centred in the 46px the 1px borders leave, so it + already sits 7px inside the padding box and 8px inside the button. Matching + that on the right is what puts the same 8px on all three sides. */ + padding: 0 7px 0 20px; border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 9999px; background: #0a0a0a; diff --git a/docs/app/(home)/cloud/observability/page.tsx b/docs/app/(home)/cloud/observability/page.tsx index 5ac0c3f27b..76a18ef425 100644 --- a/docs/app/(home)/cloud/observability/page.tsx +++ b/docs/app/(home)/cloud/observability/page.tsx @@ -1,6 +1,9 @@ +/* ssr entry: this page is a server component. */ +import { Gift } from "@phosphor-icons/react/dist/ssr"; import type { Metadata } from "next"; import Image from "next/image"; import { ExternalTextLink } from "../../components/ExternalTextLink/ExternalTextLink"; +import { FadedDither } from "../../components/FadedDither/FadedDither"; import styles from "../../page.module.css"; import type { GridFeature } from "../../sections/FeatureGridSection/FeatureGridSection"; import { Footer } from "../../sections/Footer/Footer"; @@ -83,6 +86,12 @@ export default function ObservabilityPage() { subtitle={ See what users saw and did, where your agent fell short, and what to build next. + + + + } smallSubtitle @@ -94,7 +103,10 @@ export default function ObservabilityPage() { showGitHubBanner={false} showTagline={false} desktopPreviewSlot={ - <> + /* Shader behind, artwork on top, the same stage the feature shots + use. band="light" because the hero sits straight on the page. */ +
+ - +
} mobilePreviewSlot={ + /* No stage on a phone: no shader, no fill, no rounded corners. The + art just dissolves into the page at its lower edge. */ <> OpenUI Observability insights preview - {/* Problem, then the features that answer it, then what it costs to adopt - and what it costs you in data — the Gateway page's spine. */} + {/* Problem, then the features that answer it, then the data question the + features raise, and only then what it costs to adopt. */} - + {/* Directly under Insights: the feature flow ends on what the product + learns from a session, which is the point a reader starts wondering + where that data goes. */} + + + {/* Straight after the trust block rather than below the FAQ: the ask + lands while the reader is still on the reasons to say yes. */} + +
-