From 265d0f26e45773c1f95bba5f2842be56ec3854b0 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:55:26 +0100 Subject: [PATCH 01/16] Remove leftover debug crop and duplicate CSS from immersive grid layout --- .../src/layouts/StandardLayoutArticleGrid.tsx | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 73ded291f8f..cfd5abacd98 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -221,26 +221,12 @@ export const StandardLayoutArticleGrid = ({ )} } `, - isImmersivePortrait && - css` - ${from.desktop} { - grid-template-rows: 0.25fr 1fr auto; - } - `, - isImmersiveLandscape && - css` - ${from.desktop} { - grid-template-rows: auto auto ${ageWarning - ? '130px' - : '90px'} auto auto auto auto auto; - } - `, ]} > Date: Thu, 30 Jul 2026 15:22:10 +0100 Subject: [PATCH 02/16] Fix immersive content overlapping the Labs header --- .../src/layouts/StandardLayoutArticleGrid.tsx | 12 +++++++----- dotcom-rendering/src/lib/getZIndex.test.ts | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index cfd5abacd98..383bb52c193 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -32,6 +32,7 @@ import { } from '../lib/articleFormat'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideMainMediaCaption } from '../lib/decide-caption'; +import { getZIndex } from '../lib/getZIndex'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; import { palette as themePalette } from '../palette'; @@ -260,7 +261,7 @@ export const StandardLayoutArticleGrid = ({ layoutType={layoutType} element="aside" css={css` - z-index: 100; + z-index: ${getZIndex('articleHeadline')}; `} > {/* Only show Listen to Article button on App landscape views */} diff --git a/dotcom-rendering/src/lib/getZIndex.test.ts b/dotcom-rendering/src/lib/getZIndex.test.ts index 19f82962d88..78bb441772c 100644 --- a/dotcom-rendering/src/lib/getZIndex.test.ts +++ b/dotcom-rendering/src/lib/getZIndex.test.ts @@ -14,6 +14,12 @@ describe('getZIndex', () => { expect(getZIndex('tableOfContents')).toBeGreaterThan( getZIndex('articleHeadline'), ); + expect(getZIndex('subNavBanner')).toBeGreaterThan( + getZIndex('articleHeadline'), + ); + expect(getZIndex('subNavBanner')).toBeGreaterThan( + getZIndex('bodyArea'), + ); expect(getZIndex('card-nested-link')).toBeGreaterThan( getZIndex('card-link'), ); From 996fd99b8e55b1fc04fd905118159936c6ef95de Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:42:53 +0100 Subject: [PATCH 03/16] Preserve immersive main media aspect ratios --- .../src/components/ImageBlockComponent.tsx | 4 +++ .../src/components/ImageComponent.tsx | 9 ++++-- .../src/layouts/StandardLayoutArticleGrid.tsx | 29 +++++++++++-------- dotcom-rendering/src/lib/renderElement.tsx | 3 ++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/dotcom-rendering/src/components/ImageBlockComponent.tsx b/dotcom-rendering/src/components/ImageBlockComponent.tsx index 2171767212a..fab73cf3ccd 100644 --- a/dotcom-rendering/src/components/ImageBlockComponent.tsx +++ b/dotcom-rendering/src/components/ImageBlockComponent.tsx @@ -1,3 +1,4 @@ +import type { LayoutType } from '../layouts/lib/articleArrangements'; import type { ArticleFormat } from '../lib/articleFormat'; import type { ImageBlockElement } from '../types/content'; import { ImageComponent } from './ImageComponent'; @@ -10,6 +11,7 @@ type Props = { isMainMedia?: boolean; isAvatar?: boolean; isTimeline?: boolean; + articleArrangement?: LayoutType; }; export const ImageBlockComponent = ({ @@ -20,6 +22,7 @@ export const ImageBlockComponent = ({ isMainMedia, isAvatar, isTimeline = false, + articleArrangement, }: Props) => { const { role } = element; return ( @@ -32,6 +35,7 @@ export const ImageBlockComponent = ({ title={title} isAvatar={isAvatar} isTimeline={isTimeline} + articleArrangement={articleArrangement} /> ); }; diff --git a/dotcom-rendering/src/components/ImageComponent.tsx b/dotcom-rendering/src/components/ImageComponent.tsx index acf1d4678ab..d7ad02686a9 100644 --- a/dotcom-rendering/src/components/ImageComponent.tsx +++ b/dotcom-rendering/src/components/ImageComponent.tsx @@ -9,6 +9,7 @@ import { palette as srcPalette, until, } from '@guardian/source/foundations'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { ArticleDesign, ArticleDisplay, @@ -34,6 +35,7 @@ type Props = { title?: string; isAvatar?: boolean; isTimeline?: boolean; + articleArrangement?: LayoutType; }; const timelineBulletStyles = css` @@ -244,6 +246,7 @@ export const ImageComponent = ({ title, isAvatar, isTimeline = false, + articleArrangement, }: Props) => { const { renderingTarget } = useConfig(); // Its possible the tools wont send us any images urls @@ -270,6 +273,8 @@ export const ImageComponent = ({ } const isWeb = renderingTarget === 'Web'; + const isGridImmersive = + articleArrangement?.startsWith('immersive') ?? false; /** * We use height and width for two things. @@ -303,10 +308,10 @@ export const ImageComponent = ({ always be used if display === 'immersive' */ height: 100%; width: 100%; - min-height: 25rem; + min-height: ${isGridImmersive ? 0 : '25rem'}; ${from.desktop} { - min-height: 31.25rem; + min-height: ${isGridImmersive ? 0 : '31.25rem'}; } img { diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 383bb52c193..068d60c4219 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -150,6 +150,11 @@ export const StandardLayoutArticleGrid = ({ 'model.dotcomrendering.pageElements.ImageBlockElement' ? mainMedia.media.allImages[0]?.url : undefined; + const mainMediaAspectRatio = + mainMedia?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? mainMedia.media.allImages[0]?.fields.aspectRatio + : undefined; const mainMediaOrientation = mainMediaUrl != null ? getImageOrientation(mainMediaUrl) : 'landscape'; @@ -207,18 +212,13 @@ export const StandardLayoutArticleGrid = ({ isImmersiveLandscape && css` ${from.desktop} { - grid-template-rows: auto auto ${ageWarning + grid-template-rows: auto auto ${ageWarning != null ? '130px' : '90px'} auto auto auto auto auto; ${grid.centreRule( - isImmersivePortrait - ? 4 - : isImmersiveLandscape - ? layoutType === - 'immersiveLandscapeFeature' - ? 3 - : 4 - : 1, + layoutType === 'immersiveLandscapeFeature' + ? 3 + : 4, )} } `, @@ -228,11 +228,16 @@ export const StandardLayoutArticleGrid = ({ area="media" layoutType={layoutType} css={ - isImmersiveLandscape + isImmersive ? css` + align-self: start; + ${mainMediaAspectRatio != null && + `aspect-ratio: ${mainMediaAspectRatio.replace(':', ' / ')};`} + ${from.desktop} { - margin-left: -20px; - margin-right: -20px; + ${isImmersiveLandscape && + `margin-left: -20px; + margin-right: -20px;`} } ` : undefined diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index fed8b1339dc..8414852af5f 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -180,6 +180,7 @@ export const renderElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }: Props) => { const isBlog = @@ -411,6 +412,7 @@ export const renderElement = ({ title={element.title} isAvatar={element.isAvatar} isTimeline={isTimeline} + articleArrangement={articleArrangement} /> ); case 'model.dotcomrendering.pageElements.InstagramBlockElement': @@ -1081,6 +1083,7 @@ export const RenderArticleElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }); From e96161e550653cbf3095c99b275e16fc842b05c3 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:03:54 +0100 Subject: [PATCH 04/16] Route only Labs immersive articles through the new grid --- .../src/components/ArticleHeadline.tsx | 142 ++- .../src/components/ArticleTitle.tsx | 16 +- .../src/layouts/DecideLayout.stories.tsx | 82 +- dotcom-rendering/src/layouts/DecideLayout.tsx | 26 +- .../src/layouts/ImmersiveLayout.tsx | 1022 +++++++++++++++++ dotcom-rendering/src/paletteDeclarations.ts | 19 +- 6 files changed, 1276 insertions(+), 31 deletions(-) create mode 100644 dotcom-rendering/src/layouts/ImmersiveLayout.tsx diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 0d32869f847..5d2ef46d2d3 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -28,6 +28,7 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; +import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -215,6 +216,56 @@ const invertedStyles = css` box-decoration-break: clone; `; +const legacyInvertedStyles = css` + white-space: pre-wrap; + padding-right: ${space[1]}px; + padding-bottom: ${space[1]}px; + box-shadow: -6px 0 0 ${themePalette('--headline-background')}; + /* Box decoration is required to push the box shadow out on Firefox */ + box-decoration-break: clone; +`; + +const legacyImmersiveStyles = css` + min-height: 112px; + padding-bottom: ${space[6]}px; + padding-left: ${space[1]}px; + + ${from.mobileLandscape} { + padding-left: ${space[3]}px; + } + + ${from.tablet} { + padding-left: ${space[1]}px; + } + + margin-right: ${space[5]}px; +`; + +const legacyImmersiveWrapper = css` + margin-left: 6px; + + ${from.tablet} { + margin-left: 16px; + } + + ${from.leftCol} { + margin-left: 25px; + } + + flex-grow: 1; + z-index: ${getZIndex('articleHeadline')}; + + ${until.mobileLandscape} { + margin-right: 40px; + } +`; + +const legacyInvertedText = css` + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; +`; + const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; @@ -253,13 +304,29 @@ const zIndex = css` z-index: 1; `; -const ageWarningMargins = (format: ArticleFormat) => { - if ( - format.design === ArticleDesign.Gallery || - format.display === ArticleDisplay.Immersive - ) { +const ageWarningMargins = ( + format: ArticleFormat, + isLegacyImmersive: boolean, +) => { + if (format.design === ArticleDesign.Gallery) { return ''; } + if (format.display === ArticleDisplay.Immersive) { + return isLegacyImmersive + ? css` + margin-left: 0; + margin-bottom: 0; + + ${from.tablet} { + margin-left: 10px; + } + + ${from.leftCol} { + margin-left: 20px; + } + ` + : ''; + } return css` margin-top: 12px; margin-left: -10px; @@ -282,12 +349,14 @@ const WithAgeWarning = ({ format, children, snapToInverted = false, + isLegacyImmersive = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; snapToInverted?: boolean; + isLegacyImmersive?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); @@ -296,8 +365,14 @@ const WithAgeWarning = ({ <>
{ const isInverted = layoutType === 'immersiveLandscapeDefault'; + const isLegacyImmersive = layoutType == null; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { @@ -428,13 +504,22 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - isInverted - ? [invertedText, darkBackground] - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isLegacyImmersive + ? [ + legacyInvertedText, + css` + color: ${themePalette( + '--headline-colour', + )}; + `, + ] + : isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -461,16 +546,27 @@ export const ArticleHeadline = ({ } format={format} snapToInverted={true} + isLegacyImmersive={isLegacyImmersive} >

diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index e78a65319ea..0137f523936 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -38,6 +38,18 @@ const immersiveMargins = css` } `; +const legacyImmersiveMargins = css` + max-width: 400px; + min-width: 200px; + margin-bottom: 4px; + ${from.tablet} { + margin-left: 16px; + } + ${from.leftCol} { + margin-left: 25px; + } +`; + const galleryStyles = css` ${grid.column.all} @@ -71,7 +83,9 @@ export const ArticleTitle = ({
diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index e65424200ac..39a46821af7 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -229,14 +229,88 @@ export const AppsPictureShowcaseOpinionDark: Story = { * * Example: https://www.chromatic.com/test?appId=63e251470cfbe61776b0ef19&id=675aaa4f3aa384bd64bde3a1 */ +const photoEssayImmersiveLabsArticle = enhanceArticleType( + PhotoEssayImmersiveLabsFixture, + 'Web', +); + +const portraitMainMedia = PhotoEssayImmersiveLabsFixture.blocks + .flatMap((block) => block.elements) + .find( + (element) => + element._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' && + element.media.allImages[0]?.fields.aspectRatio === '4:5', + ); + +if (portraitMainMedia == null) { + throw new Error('The Labs fixture must contain a portrait image'); +} + +const photoEssayImmersiveLabsPortraitArticle = enhanceArticleType( + { + ...PhotoEssayImmersiveLabsFixture, + mainMediaElements: [{ ...portraitMainMedia, role: 'immersive' }], + }, + 'Web', +); + +const labsImmersiveArticle = ({ + orientation, + design, +}: { + orientation: 'portrait' | 'landscape'; + design: ArticleDesign.PhotoEssay | ArticleDesign.Feature; +}): Article => ({ + ...(orientation === 'portrait' + ? photoEssayImmersiveLabsPortraitArticle + : photoEssayImmersiveLabsArticle), + design, +}); + +const immersiveLabsParameters = { + ...webParameters, + chromatic: { disableSnapshot: true }, +}; + export const WebPhotoEssayImmersiveLabsLight: Story = { args: { - article: enhanceArticleType(PhotoEssayImmersiveLabsFixture, 'Web'), + article: labsImmersiveArticle({ + orientation: 'landscape', + design: ArticleDesign.PhotoEssay, + }), }, - parameters: { - ...webParameters, - chromatic: { disableSnapshot: true }, + parameters: immersiveLabsParameters, +}; + +export const WebPhotoEssayImmersiveLabsPortraitLight: Story = { + args: { + article: labsImmersiveArticle({ + orientation: 'portrait', + design: ArticleDesign.PhotoEssay, + }), + }, + parameters: immersiveLabsParameters, +}; + +export const WebFeatureImmersiveLabsLandscapeLight: Story = { + args: { + article: labsImmersiveArticle({ + orientation: 'landscape', + design: ArticleDesign.Feature, + }), + }, + parameters: immersiveLabsParameters, +}; + +export const WebFeatureImmersiveLabsPortraitLight: Story = { + args: { + article: labsImmersiveArticle({ + orientation: 'portrait', + design: ArticleDesign.Feature, + }), }, + parameters: immersiveLabsParameters, }; const standardStandardLabsWebFixture: Article = { diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index 3d828bdc45b..2a763cf1e56 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -1,4 +1,8 @@ -import { ArticleDesign, ArticleDisplay } from '../lib/articleFormat'; +import { + ArticleDesign, + ArticleDisplay, + ArticleSpecial, +} from '../lib/articleFormat'; import type { NavType } from '../model/extract-nav'; import type { Article } from '../types/article'; import type { RenderingTarget } from '../types/renderingTarget'; @@ -9,6 +13,7 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; +import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -53,13 +58,20 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { ); } default: { - return ( + return article.theme === ArticleSpecial.Labs ? ( + ) : ( + ); } } @@ -229,7 +241,7 @@ const DecideLayoutWeb = ({ article, NAV, renderingTarget }: WebProps) => { ); } default: { - return ( + return article.theme === ArticleSpecial.Labs ? ( { renderingTarget={renderingTarget} serverTime={serverTime} /> + ) : ( + ); } } diff --git a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx new file mode 100644 index 00000000000..03779fc4c3a --- /dev/null +++ b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx @@ -0,0 +1,1022 @@ +import { css } from '@emotion/react'; +import { + from, + palette as sourcePalette, + space, + until, +} from '@guardian/source/foundations'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; +import { AdPortals } from '../components/AdPortals.island'; +import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; +import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; +import { AppsFooter } from '../components/AppsFooter.island'; +import { ArticleBody } from '../components/ArticleBody'; +import { ArticleContainer } from '../components/ArticleContainer'; +import { ArticleHeadline } from '../components/ArticleHeadline'; +import { ArticleMetaApps } from '../components/ArticleMeta.apps'; +import { ArticleMeta } from '../components/ArticleMeta.web'; +import { ArticleTitle } from '../components/ArticleTitle'; +import { Border } from '../components/Border'; +import { Caption } from '../components/Caption'; +import { Carousel } from '../components/Carousel.island'; +import { DecideLines } from '../components/DecideLines'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; +import { DiscussionLayout } from '../components/DiscussionLayout'; +import { Footer } from '../components/Footer'; +import { GridItem } from '../components/GridItem'; +import { GuardianLabsLines } from '../components/GuardianLabsLines'; +import { HeadlineByline } from '../components/HeadlineByline'; +import { Hide } from '../components/Hide'; +import { Island } from '../components/Island'; +import { LabsHeader } from '../components/LabsHeader'; +import { ListenToArticle } from '../components/ListenToArticle.island'; +import { MainMedia } from '../components/MainMedia'; +import { Masthead } from '../components/Masthead/Masthead'; +import { minHeaderHeightPx } from '../components/Masthead/Titlepiece/constants'; +import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; +import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; +import { OnwardsUpper } from '../components/OnwardsUpper.island'; +import { RightColumn } from '../components/RightColumn'; +import { Section } from '../components/Section'; +import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; +import { Standfirst } from '../components/Standfirst'; +import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; +import { SubMeta } from '../components/SubMeta'; +import { SubNav } from '../components/SubNav.island'; +import { + ArticleDesign, + type ArticleFormat, + ArticleSpecial, +} from '../lib/articleFormat'; +import { canRenderAds } from '../lib/canRenderAds'; +import { getContributionsServiceUrl } from '../lib/contributions'; +import { decideMainMediaCaption } from '../lib/decide-caption'; +import { decideStoryPackageTrails } from '../lib/decideTrail'; +import { getZIndex } from '../lib/getZIndex'; +import { LABS_HEADER_HEIGHT } from '../lib/labs-constants'; +import { parse } from '../lib/slot-machine-flags'; +import type { NavType } from '../model/extract-nav'; +import { palette as themePalette } from '../palette'; +import type { ArticleDeprecated } from '../types/article'; +import { BannerWrapper, Stuck } from './lib/stickiness'; + +const ImmersiveGrid = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+); + +const maxWidth = css` + ${from.desktop} { + max-width: 620px; + } +`; + +const linesMargin = css` + ${from.leftCol} { + margin-top: ${space[5]}px; + } +`; + +const stretchLines = css` + ${until.phablet} { + margin-left: -20px; + margin-right: -20px; + } + ${until.mobileLandscape} { + margin-left: -10px; + margin-right: -10px; + } +`; + +interface CommonProps { + article: ArticleDeprecated; + format: ArticleFormat; + serverTime?: number; +} + +interface WebProps extends CommonProps { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppProps extends CommonProps { + renderingTarget: 'Apps'; +} + +const Box = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+); + +export const ImmersiveLayout = (props: WebProps | AppProps) => { + const { article, format, renderingTarget, serverTime } = props; + + const { + config: { isPaidContent, host, hasSurveyAd }, + editionId, + } = article; + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + + const showBodyEndSlot = + isWeb && + (parse(article.slotMachineFlags ?? '').showBodyEnd || + article.config.switches.slotBodyEnd); + + // TODO: + // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render + // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. + + const showComments = article.isCommentable && !isPaidContent; + + const mainMedia = article.mainMediaElements[0]; + + const captionText = decideMainMediaCaption(mainMedia); + + const HEADLINE_OFFSET = mainMedia ? 120 : 0; + + const { branding } = article.commercialProperties[article.editionId]; + + const contributionsServiceUrl = getContributionsServiceUrl(article); + + const isLabs = format.theme === ArticleSpecial.Labs; + + /** + We need change the height values depending on whether the labs header is there or not to keep + the headlines appearing at a consistent height between labs and non labs immersive articles. + */ + + const labsHeaderHeight = LABS_HEADER_HEIGHT; + const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); + + const navAndLabsHeaderHeight = isLabs + ? `${combinedHeight}px` + : `${minHeaderHeightPx}px`; + + const hasMainMediaStyles = css` + height: calc(80vh - ${navAndLabsHeaderHeight}); + /** + 80vh is normally enough but don't let the content shrink vertically too + much just in case + */ + min-height: calc(25rem - ${navAndLabsHeaderHeight}); + ${from.desktop} { + height: calc(100vh - ${navAndLabsHeaderHeight}); + min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); + } + ${from.wide} { + min-height: calc(50rem - ${navAndLabsHeaderHeight}); + } + `; + const LeftColCaption = () => ( +
+ +
+ ); + + const renderAds = canRenderAds(article); + + return ( + <> + {isWeb && ( + tag.id)} + sectionId={article.config.section} + contentType={article.contentType} + /> + )} + + + + {format.theme === ArticleSpecial.Labs && ( + +
+ +
+
+ )} + +
+
+ +
+ {mainMedia && ( + <> +
+
} + > + +
+ +
+ +
+
+
+ + )} +
+ + {isWeb && renderAds && hasSurveyAd && ( + + )} + +
+ {isApps && renderAds && ( + + + + )} +
+ + {/* Above leftCol, the Caption is controlled by Section ^^ */} + + + + + + + {format.design === ArticleDesign.PhotoEssay ? ( + <> + ) : ( + + )} + + + <> + {!mainMedia && ( +
+ +
+ )} + +
+ + <> + {!mainMedia && ( +
+ +
+ )} + +
+ + + + + {!!article.byline && ( + + )} + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + +
+ + + +
+
+ )} +
+ + {format.design === ArticleDesign.PhotoEssay && + !isLabs ? ( + <> + ) : ( +
+
+ {format.theme === + ArticleSpecial.Labs ? ( + + ) : ( + + )} +
+
+ )} +
+ {isApps ? ( + <> + + + + + + {!!article.affiliateLinksDisclaimer && ( + + )} + + + ) : ( + <> + + {!!article.affiliateLinksDisclaimer && ( + + )} + + )} +
+
+ + + + {showBodyEndSlot && ( + + + + )} + + + + + +
+ + <> + {mainMedia && isWeb && renderAds && ( +
+ { + + } +
+ )} + +
+
+
+
+
+ {!isLabs && isWeb && renderAds && ( +
+ +
+ )} + + {article.storyPackage && ( +
+ + + +
+ )} + + + + + + {showComments && ( +
+ +
+ )} + {!isPaidContent && ( +
+ + + + + +
+ )} + {!isLabs && isWeb && renderAds && ( +
+ +
+ )} +
+ + {isWeb && props.NAV.subNavSections && ( +
+ + + +
+ )} + + {isWeb && ( + <> +
+
+
+ + + + + + + {renderAds && ( + + )} + + )} + {isApps && ( +
+ + + +
+ )} + + ); +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index b0bf5c22d02..6efcfbf0033 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,9 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[7]; + return theme === ArticleSpecial.Labs + ? sourcePalette.neutral[7] + : sourcePalette.neutral[97]; default: { switch (design) { case ArticleDesign.Editorial: @@ -195,8 +197,21 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { +const headlineBackgroundLight: PaletteFunction = ({ + display, + design, + theme, +}) => { switch (display) { + case ArticleDisplay.Immersive: + switch (theme) { + case ArticleSpecial.Labs: + return 'transparent'; + case ArticleSpecial.SpecialReport: + return sourcePalette.specialReport[300]; + default: + return sourcePalette.neutral[7]; + } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: From 12c9356a6525cefeff7236c907d87e8ce44e9c92 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:11:03 +0100 Subject: [PATCH 05/16] Update DecideLayout.stories.tsx --- dotcom-rendering/src/layouts/DecideLayout.stories.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index 39a46821af7..db337c329ed 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -27,6 +27,7 @@ import { import { getCurrentPillar } from '../lib/layoutHelpers'; import { extractNAV } from '../model/extract-nav'; import { type Article, enhanceArticleType } from '../types/article'; +import type { ImageBlockElement } from '../types/content'; import { DecideLayout, type Props as DecideLayoutProps } from './DecideLayout'; export type HydratedLayoutDecoratorArgs = { @@ -237,7 +238,7 @@ const photoEssayImmersiveLabsArticle = enhanceArticleType( const portraitMainMedia = PhotoEssayImmersiveLabsFixture.blocks .flatMap((block) => block.elements) .find( - (element) => + (element): element is ImageBlockElement => element._type === 'model.dotcomrendering.pageElements.ImageBlockElement' && element.media.allImages[0]?.fields.aspectRatio === '4:5', @@ -250,7 +251,7 @@ if (portraitMainMedia == null) { const photoEssayImmersiveLabsPortraitArticle = enhanceArticleType( { ...PhotoEssayImmersiveLabsFixture, - mainMediaElements: [{ ...portraitMainMedia, role: 'immersive' }], + mainMediaElements: [portraitMainMedia], }, 'Web', ); From 09f4a738d6dc57b3ad51da698be7fef5fde74d8f Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:42:20 +0100 Subject: [PATCH 06/16] Update playwright assertion --- dotcom-rendering/playwright/tests/commercial.e2e.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts index db823202659..61c96351056 100644 --- a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts @@ -19,7 +19,7 @@ test.describe('Commercial E2E tests', () => { 'merchandising', ]; - const totalSlotsExpected = 16; // All slots, even if unfilled ie. `display: none` + const totalSlotsExpected = 15; // All slots, even if unfilled ie. `display: none` const inlineSlots = totalSlotsExpected - fixedSlots.length; // We are excluding survey slot as they can be switched off From ce1eafab2470593ac8406e9992b290c44f76b895 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 3 Aug 2026 15:28:58 +0100 Subject: [PATCH 07/16] align title to bottom of container --- dotcom-rendering/src/components/ArticleTitle.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index 0137f523936..e71a624b749 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -63,6 +63,11 @@ const galleryStyles = css` } `; +const immersivePortraitStyles = css` + height: 100%; + justify-content: flex-end; +`; + export const ArticleTitle = ({ format, layoutType, @@ -77,6 +82,9 @@ export const ArticleTitle = ({ [ArticleDesign.Gallery, ArticleDesign.HostedGallery].includes( format.design, ) && galleryStyles, + (layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature') && + immersivePortraitStyles, sectionStyles, ]} > From 52297b57667c05d9dbab503630e73e66c7b3ebbd Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 3 Aug 2026 15:59:43 +0100 Subject: [PATCH 08/16] fix standfirst font-size --- .../src/components/ArticleTitle.tsx | 6 +++-- .../src/components/Standfirst.tsx | 27 ++++++++++++++++--- .../src/layouts/StandardLayoutArticleGrid.tsx | 6 ++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index e71a624b749..fabfa38e4ea 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -64,8 +64,10 @@ const galleryStyles = css` `; const immersivePortraitStyles = css` - height: 100%; - justify-content: flex-end; + ${from.desktop} { + height: 100%; + justify-content: flex-end; + } `; export const ArticleTitle = ({ diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index c517894fcaa..e5192d2919d 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -15,6 +15,7 @@ import { } from '@guardian/source/foundations'; import sanitise from 'sanitize-html'; import { grid } from '../../src/grid'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { ArticleDesign, @@ -27,6 +28,7 @@ import { palette } from '../palette'; type Props = { format: ArticleFormat; standfirst: string; + layoutType: LayoutType; }; const nestedStyles = (format: ArticleFormat) => { @@ -79,8 +81,16 @@ const nestedStyles = (format: ArticleFormat) => { `; }; -const decideFont = ({ display, design, theme }: ArticleFormat) => { +const decideFont = ( + { display, design, theme }: ArticleFormat, + layoutType: LayoutType, +) => { const isLabs = theme === ArticleSpecial.Labs; + const isImmersivePortraitOrLandscape = + layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature' || + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'; switch (design) { case ArticleDesign.Gallery: if (isLabs) { @@ -109,6 +119,11 @@ const decideFont = ({ display, design, theme }: ArticleFormat) => { } `; } + if (isImmersivePortraitOrLandscape) { + return css` + ${headlineMedium20}; + `; + } return css` ${headlineLight20}; ${from.tablet} { @@ -145,6 +160,11 @@ const decideFont = ({ display, design, theme }: ArticleFormat) => { } `; } + if (isImmersivePortraitOrLandscape) { + return css` + ${headlineMedium20}; + `; + } return css` ${headlineMedium20}; ${from.tablet} { @@ -386,8 +406,7 @@ const hoverStyles = css` border-bottom: solid 1px ${palette('--standfirst-link-border')}; } `; - -export const Standfirst = ({ format, standfirst }: Props) => { +export const Standfirst = ({ format, standfirst, layoutType }: Props) => { if (standfirst.trim() === '') { return null; } @@ -397,7 +416,7 @@ export const Standfirst = ({ format, standfirst }: Props) => { css={[ nestedStyles(format), standfirstStyles(format), - decideFont(format), + decideFont(format, layoutType), decidePadding(format), hoverStyles, ]} diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index b629d40bb89..59719fd491b 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -329,7 +329,11 @@ export const StandardLayoutArticleGrid = ({ `, ]} > - + {isImmersive && ( Date: Mon, 3 Aug 2026 16:01:20 +0100 Subject: [PATCH 09/16] make layoutType optional for Standfirst --- dotcom-rendering/src/components/Standfirst.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index e5192d2919d..6aa6ae32173 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -28,7 +28,7 @@ import { palette } from '../palette'; type Props = { format: ArticleFormat; standfirst: string; - layoutType: LayoutType; + layoutType?: LayoutType; }; const nestedStyles = (format: ArticleFormat) => { From 9dbab9d0086c472856dca51cd55fabdc83c86834 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 3 Aug 2026 16:05:13 +0100 Subject: [PATCH 10/16] make layoutType optional for Standfirst styling --- dotcom-rendering/src/components/Standfirst.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 6aa6ae32173..90ae22feac3 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -83,14 +83,15 @@ const nestedStyles = (format: ArticleFormat) => { const decideFont = ( { display, design, theme }: ArticleFormat, - layoutType: LayoutType, + layoutType?: LayoutType, ) => { const isLabs = theme === ArticleSpecial.Labs; const isImmersivePortraitOrLandscape = - layoutType === 'immersiveLandscapeDefault' || - layoutType === 'immersiveLandscapeFeature' || - layoutType === 'immersivePortraitDefault' || - layoutType === 'immersivePortraitFeature'; + layoutType && + (layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature' || + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'); switch (design) { case ArticleDesign.Gallery: if (isLabs) { From d3fda354c25208f4b54801b810fd14c55fa7e279 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 3 Aug 2026 17:11:52 +0100 Subject: [PATCH 11/16] headline and standfirst colour --- .../src/components/ArticleHeadline.tsx | 19 ++++++++--- .../src/components/ArticleMeta.web.tsx | 5 +-- .../src/components/SeriesSectionLink.tsx | 7 +--- .../src/components/Standfirst.tsx | 20 ++++++----- dotcom-rendering/src/paletteDeclarations.ts | 34 +++++++++++++++++++ 5 files changed, 61 insertions(+), 24 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 5d2ef46d2d3..81abeb9a0e0 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -478,6 +478,9 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = layoutType === 'immersiveLandscapeDefault'; + const isImmersivePortrait = + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'; const isLegacyImmersive = layoutType == null; switch (format.display) { case ArticleDisplay.Immersive: { @@ -562,11 +565,17 @@ export const ArticleHeadline = ({ ] : isInverted ? [invertedText, darkBackground] - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + : isImmersivePortrait + ? css` + color: ${themePalette( + '--immersive-portrait-headline-text', + )}; + ` + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > ); } - if ( - layoutType === 'immersivePortraitDefault' || - layoutType === 'immersivePortraitFeature' || - layoutType === 'immersiveLandscapeDefault' || - layoutType === 'immersiveLandscapeFeature' - ) { + if (layoutType?.startsWith('immersive') ?? false) { return ( <> { const isLabs = theme === ArticleSpecial.Labs; const isImmersivePortraitOrLandscape = - layoutType && - (layoutType === 'immersiveLandscapeDefault' || - layoutType === 'immersiveLandscapeFeature' || - layoutType === 'immersivePortraitDefault' || - layoutType === 'immersivePortraitFeature'); + layoutType?.startsWith('immersive') ?? false; switch (design) { case ArticleDesign.Gallery: if (isLabs) { @@ -275,7 +271,12 @@ const decidePadding = ({ display, design }: ArticleFormat) => { } }; -const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { +const standfirstStyles = ( + { display, design, theme }: ArticleFormat, + layoutType?: LayoutType, +) => { + const isImmersivePortraitOrLandscape = + layoutType?.startsWith('immersive') ?? false; switch (display) { case ArticleDisplay.Immersive: switch (design) { @@ -290,14 +291,15 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { ${from.tablet} { max-width: 460px; } - color: ${palette('--standfirst-text')}; + color: ${isImmersivePortraitOrLandscape + ? palette('--immersive-portrait-standfirst-text') + : palette('--standfirst-text')}; li::before { height: 17px; width: 17px; } `; } - case ArticleDisplay.NumberedList: return css` max-width: 540px; @@ -416,7 +418,7 @@ export const Standfirst = ({ format, standfirst, layoutType }: Props) => {
{ } }; +const immersivePortraitHeadlineLight: PaletteFunction = () => + sourcePalette.neutral[0]; + +const immersivePortraitHeadlineDark: PaletteFunction = () => + sourcePalette.neutral[100]; + +const immersivePortraitStandfirstLight: PaletteFunction = (format) => { + switch (format.theme) { + case Pillar.Sport: + case Pillar.Lifestyle: + case Pillar.Culture: + return pillarPalette(format.theme, 200); + default: + return sourcePalette.neutral[0]; + } +}; +const immersivePortraitStandfirstDark: PaletteFunction = (format) => { + switch (format.theme) { + case Pillar.Sport: + case Pillar.Lifestyle: + case Pillar.Culture: + return pillarPalette(format.theme, 600); + default: + return sourcePalette.neutral[100]; + } +}; const bioLinkUnderline: PaletteFunction = () => sourcePalette.neutral[86]; const multiBylineNonLinkedTextLight: PaletteFunction = () => @@ -7486,6 +7512,14 @@ const paletteColours = { light: imageTitleBackground, dark: imageTitleBackground, }, + '--immersive-portrait-headline-text': { + light: immersivePortraitHeadlineLight, + dark: immersivePortraitHeadlineDark, + }, + '--immersive-portrait-standfirst-text': { + light: immersivePortraitStandfirstLight, + dark: immersivePortraitStandfirstDark, + }, '--interactive-atom-background': { light: interactiveAtomBackgroundLight, dark: interactiveAtomBackgroundDark, From e1ff0370ee364ac3015e9c6458f0b1a84559d2da Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 3 Aug 2026 17:57:27 +0100 Subject: [PATCH 12/16] fix title text colour and background --- .../src/components/ArticleTitle.tsx | 17 +++++++- .../src/components/SeriesSectionLink.tsx | 4 +- dotcom-rendering/src/paletteDeclarations.ts | 42 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index fabfa38e4ea..3810a8e1d0e 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -7,6 +7,7 @@ import { ArticleDisplay, type ArticleFormat, } from '../lib/articleFormat'; +import { palette as themePalette } from '../palette'; import type { TagType } from '../types/tag'; import { SeriesSectionLink } from './SeriesSectionLink'; @@ -70,6 +71,14 @@ const immersivePortraitStyles = css` } `; +const immersivePortraitSeriesSectionWrapperStyles = css` + padding: 3px 6px 4px; + margin-bottom: 0; + background-color: ${themePalette('--immersive-portrait-title-background')}; + width: fit-content; + min-width: unset; +`; + export const ArticleTitle = ({ format, layoutType, @@ -95,7 +104,13 @@ export const ArticleTitle = ({ format.display === ArticleDisplay.Immersive ? layoutType == null ? legacyImmersiveMargins - : immersiveMargins + : layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature' + ? [ + immersiveMargins, + immersivePortraitSeriesSectionWrapperStyles, + ] + : immersiveMargins : undefined } > diff --git a/dotcom-rendering/src/components/SeriesSectionLink.tsx b/dotcom-rendering/src/components/SeriesSectionLink.tsx index 7357df1d9be..2b9f312c707 100644 --- a/dotcom-rendering/src/components/SeriesSectionLink.tsx +++ b/dotcom-rendering/src/components/SeriesSectionLink.tsx @@ -387,7 +387,9 @@ export const SeriesSectionLink = ({ css={[ sectionLabelLink, css` - color: ${titleColour}; + color: ${themePalette( + '--immersive-portrait-title-text', + )}; background-color: ${themePalette( '--section-title-background', )}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 9580b7f119c..d6ff81153d6 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -14,6 +14,7 @@ import { tabsDarkTheme, tabsThemeDefault, } from '@guardian/source-development-kitchen/react-components'; +import format from 'ajv/dist/vocabularies/format/format'; import { ArticleDesign, ArticleDisplay, @@ -6058,6 +6059,7 @@ const immersivePortraitStandfirstLight: PaletteFunction = (format) => { return sourcePalette.neutral[0]; } }; + const immersivePortraitStandfirstDark: PaletteFunction = (format) => { switch (format.theme) { case Pillar.Sport: @@ -6068,6 +6070,38 @@ const immersivePortraitStandfirstDark: PaletteFunction = (format) => { return sourcePalette.neutral[100]; } }; + +const immersivePortraitTitleBackgroundLight: PaletteFunction = (format) => { + switch (format.theme) { + case Pillar.News: + case Pillar.Opinion: + case Pillar.Sport: + case Pillar.Lifestyle: + case Pillar.Culture: + return pillarPalette(format.theme, 400); + default: + return sourcePalette.neutral[100]; + } +}; + +const immersivePortraitTitleBackgroundDark: PaletteFunction = (format) => { + switch (format.theme) { + case Pillar.News: + case Pillar.Opinion: + case Pillar.Sport: + case Pillar.Lifestyle: + case Pillar.Culture: + return pillarPalette(format.theme, 500); + default: + return sourcePalette.neutral[7]; + } +}; + +const immersivePortraitTitleTextLight: PaletteFunction = () => + sourcePalette.neutral[100]; +const immersivePortraitTitleTextDark: PaletteFunction = () => + sourcePalette.neutral[7]; + const bioLinkUnderline: PaletteFunction = () => sourcePalette.neutral[86]; const multiBylineNonLinkedTextLight: PaletteFunction = () => @@ -7520,6 +7554,14 @@ const paletteColours = { light: immersivePortraitStandfirstLight, dark: immersivePortraitStandfirstDark, }, + '--immersive-portrait-title-background': { + light: immersivePortraitTitleBackgroundLight, + dark: immersivePortraitTitleBackgroundDark, + }, + '--immersive-portrait-title-text': { + light: immersivePortraitTitleTextLight, + dark: immersivePortraitTitleTextDark, + }, '--interactive-atom-background': { light: interactiveAtomBackgroundLight, dark: interactiveAtomBackgroundDark, From 28db79b45805b265723c16e32121ccbc253f4c3f Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 3 Aug 2026 17:58:51 +0100 Subject: [PATCH 13/16] remove unused import --- dotcom-rendering/src/paletteDeclarations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index d6ff81153d6..c3d04bd79ff 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -14,7 +14,6 @@ import { tabsDarkTheme, tabsThemeDefault, } from '@guardian/source-development-kitchen/react-components'; -import format from 'ajv/dist/vocabularies/format/format'; import { ArticleDesign, ArticleDisplay, From be016394f75c196a53f11b45b6787698fab5aee8 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 10 Aug 2026 17:27:09 +0100 Subject: [PATCH 14/16] style title for immersive portrait layout --- dotcom-rendering/src/components/ArticleTitle.tsx | 2 +- dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index 3810a8e1d0e..0cf90f25396 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -65,7 +65,7 @@ const galleryStyles = css` `; const immersivePortraitStyles = css` - ${from.desktop} { + ${from.leftCol} { height: 100%; justify-content: flex-end; } diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 59719fd491b..7a100b97bfc 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -267,6 +267,13 @@ export const StandardLayoutArticleGrid = ({ element="aside" css={css` z-index: ${getZIndex('articleHeadline')}; + + ${from.desktop} { + margin: auto 0 0; + } + ${from.leftCol} { + margin: 0; + } `} > Date: Mon, 10 Aug 2026 17:27:37 +0100 Subject: [PATCH 15/16] style headline and meta for immersive portrait layout --- .../src/components/ArticleHeadline.tsx | 55 ++++++++++++++++++- .../src/components/ArticleMeta.web.tsx | 22 +++++++- .../src/layouts/lib/articleArrangements.ts | 4 +- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 81abeb9a0e0..33edd09992f 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -123,7 +123,7 @@ const decideMobileHeadlineFont = (format: ArticleFormat) => { } }; -const headlineFont = (format: ArticleFormat) => { +const headlineFont = (format: ArticleFormat, layoutType?: LayoutType) => { if (format.design === ArticleDesign.Gallery) { return css` ${decideMobileHeadlineFont(format)} @@ -132,6 +132,17 @@ const headlineFont = (format: ArticleFormat) => { } `; } + if ( + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature' + ) { + return css` + ${decideMobileHeadlineFont(format)} + ${from.leftCol} { + ${decideHeadlineFont(format)} + } + `; + } return css` ${decideMobileHeadlineFont(format)} ${from.tablet} { @@ -343,6 +354,40 @@ const ageWarningMargins = ( `; }; +const gridColumns = css` + ${from.desktop} { + grid-column: 1 / 6; + } + ${from.leftCol} { + grid-column: 1 / 7; + } + ${from.wide} { + grid-column: 1 / 8; + } +`; + +const displayGrid = css` + ${from.desktop} { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-gap: 0.25fr; + } + ${from.leftCol} { + grid-template-columns: repeat(7, 1fr); + } + ${from.wide} { + grid-template-columns: repeat(8, 1fr); + } +`; + +const paddingBottom = css` + padding-bottom: ${space[6]}px; + + ${from.desktop} { + padding-bottom: 0; + } +`; + const WithAgeWarning = ({ tags, webPublicationDateDeprecated, @@ -576,19 +621,25 @@ export const ArticleHeadline = ({ '--headline-colour', )}; `, + isImmersivePortrait && [ + , + displayGrid, + paddingBottom, + ], ]} > {headlineString} diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index f0d1622444e..4e51aaaeb42 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -289,6 +289,22 @@ const metaNumbersExtrasLiveBlog = css` } `; +const borderTop = css` + border-top: 1px solid ${themePalette('--article-border')}; + + ${from.desktop} { + border-top: none; + } +`; + +const paddingTop = css` + padding-top: ${space[3]}px; + + ${from.leftCol} { + padding-top: 0; + } +`; + export const ArticleMeta = ({ branding, format, @@ -345,7 +361,11 @@ export const ArticleMeta = ({ className={ isInteractive ? interactiveLegacyClasses.metaContainer : '' } - css={metaContainer(format)} + css={[ + metaContainer(format), + isPortraitOrLandscapeImmersive && borderTop, + isPortraitOrLandscapeImmersive && paddingTop, + ]} >
{branding && ( diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index e7fee10daec..88f10d170dc 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -182,7 +182,7 @@ const immersivePortraitDefaultCss: LayoutCssMap = { standfirst: { mobile: 'grid-row: 4;', tablet: 'grid-row: 4;', - desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + desktop: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, }, @@ -231,7 +231,7 @@ const immersivePortraitFeatureCss: LayoutCssMap = { standfirst: { mobile: 'grid-row: 4;', tablet: 'grid-row: 4;', - desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + desktop: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, }, From 1c7d5eb28d5f573f30c46af500d611027e72a5f9 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Mon, 10 Aug 2026 17:39:39 +0100 Subject: [PATCH 16/16] remove wild comma from styling array --- dotcom-rendering/src/components/ArticleHeadline.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 33edd09992f..7016eb685e7 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -622,7 +622,6 @@ export const ArticleHeadline = ({ )}; `, isImmersivePortrait && [ - , displayGrid, paddingBottom, ],