Grid and list homepage reading surface for heise.de
// ==UserScript==
// @name Heise Editorial Layout
// @namespace local.heise-editorial-layout
// @version 0.6.0
// @description Grid and list homepage reading surface for heise.de
// @match https://www.heise.de/
// @run-at document-idle
// @grant none
// @license MIT
// @author Trbtxc
// ==/UserScript==
(function () {
'use strict';
const ROOT_ID = 'heise-editorial-layout';
const STYLE_ID = 'heise-editorial-layout-styles';
const VIEW_STORAGE_KEY = 'heise-editorial-layout:view';
const DISABLED_BRANDINGS_STORAGE_KEY =
'heise-editorial-layout:disabled-brandings';
const HIDE_PAYWALLED_STORAGE_KEY = 'heise-editorial-layout:hide-paywalled';
const DEFAULT_VIEW = 'grid';
const GRID_ARTICLE_LIMIT = 6;
const OBSERVER_TIMEOUT_MS = 5000;
const THEME_DARK = 'dark';
const THEME_LIGHT = 'light';
const TARGET_SELECTOR =
'[data-component="ModulesContainer"][data-upscore-zone="ho_homepage_full"]';
const FEATURED_SELECTOR =
'[data-component="TeasersModule"] section[data-layout-name="FreeHorizontalLayout"]';
const MAIN_SELECTOR =
'[data-component="TeasersModule"] section[data-layout="TeaserListLayout"]';
const CARD_SELECTOR = 'article[data-component="TeaserContainer"]';
const MAIN_LIST_CARD_SELECTOR =
':scope > [data-component="TeaserList"] > article[data-component="TeaserContainer"]';
const dateFormatter = new Intl.DateTimeFormat('de-DE', {
dateStyle: 'medium',
timeStyle: 'short',
});
if (document.getElementById(ROOT_ID)) {
return;
}
let observer = null;
let timeoutId = null;
let retryFrameId = null;
let themeObserver = null;
let themeMediaQuery = null;
let themeSyncFrameId = null;
let themeRoot = null;
let mounted = false;
let lastFailure = 'required homepage content was not ready';
function parseRgbColor(colorValue) {
if (typeof colorValue !== 'string') {
return null;
}
const normalized = colorValue.trim().toLowerCase();
if (!normalized || normalized === 'transparent') {
return null;
}
const rgbMatch = normalized.match(
/^rgba?\((\d+)\s*[\s,/]\s*(\d+)\s*[\s,/]\s*(\d+)(?:\s*[\s,/]\s*([\d.]+))?\)$/
);
if (rgbMatch) {
const alpha = rgbMatch[4] !== undefined ? Number(rgbMatch[4]) : 1;
if (Number.isFinite(alpha) && alpha <= 0) {
return null;
}
return [Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3])];
}
const hexMatch = normalized.match(/^#([\da-f]{3}|[\da-f]{6})$/i);
if (hexMatch) {
const hex = hexMatch[1];
if (hex.length === 3) {
return [
parseInt(hex[0] + hex[0], 16),
parseInt(hex[1] + hex[1], 16),
parseInt(hex[2] + hex[2], 16),
];
}
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
];
}
return null;
}
function getColorLuminance(rgbColor) {
return 0.2126 * rgbColor[0] + 0.7152 * rgbColor[1] + 0.0722 * rgbColor[2];
}
function getEffectiveTheme() {
const html = document.documentElement;
if (html.classList.contains(THEME_DARK)) {
return THEME_DARK;
}
if (html.classList.contains(THEME_LIGHT)) {
return THEME_LIGHT;
}
const bodyStyle = window.getComputedStyle(document.body);
const htmlStyle = window.getComputedStyle(html);
const sampledColor =
parseRgbColor(bodyStyle.backgroundColor) ??
parseRgbColor(htmlStyle.backgroundColor);
if (sampledColor) {
return getColorLuminance(sampledColor) < 140 ? THEME_DARK : THEME_LIGHT;
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return THEME_DARK;
}
return THEME_LIGHT;
}
function applyTheme(root, theme) {
root.dataset.helTheme = theme === THEME_DARK ? THEME_DARK : THEME_LIGHT;
}
function scheduleThemeSync() {
if (!themeRoot || themeSyncFrameId !== null) {
return;
}
themeSyncFrameId = requestAnimationFrame(() => {
themeSyncFrameId = null;
if (!themeRoot) {
return;
}
applyTheme(themeRoot, getEffectiveTheme());
});
}
function setupThemeSync(root) {
themeRoot = root;
applyTheme(root, getEffectiveTheme());
themeObserver?.disconnect();
themeObserver = new MutationObserver(scheduleThemeSync);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'data-theme'],
});
themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof themeMediaQuery.addEventListener === 'function') {
themeMediaQuery.addEventListener('change', scheduleThemeSync);
} else if (typeof themeMediaQuery.addListener === 'function') {
themeMediaQuery.addListener(scheduleThemeSync);
}
document.addEventListener('visibilitychange', scheduleThemeSync);
window.addEventListener('pageshow', scheduleThemeSync);
}
function readStoredView() {
try {
const storedView = localStorage.getItem(VIEW_STORAGE_KEY);
return storedView === 'grid' || storedView === 'list'
? storedView
: DEFAULT_VIEW;
} catch {
return DEFAULT_VIEW;
}
}
function storeView(view) {
try {
localStorage.setItem(VIEW_STORAGE_KEY, view);
} catch {
// Persistence is optional; view switching remains available in memory.
}
}
function readDisabledBrandings() {
try {
const storedBrandings = JSON.parse(
localStorage.getItem(DISABLED_BRANDINGS_STORAGE_KEY) ?? '[]'
);
return new Set(
Array.isArray(storedBrandings)
? storedBrandings.filter((branding) => typeof branding === 'string')
: []
);
} catch {
return new Set();
}
}
function storeDisabledBrandings(disabledBrandings) {
try {
localStorage.setItem(
DISABLED_BRANDINGS_STORAGE_KEY,
JSON.stringify(Array.from(disabledBrandings))
);
} catch {
// Persistence is optional; filtering remains available in memory.
}
}
function readHidePaywalled() {
try {
return localStorage.getItem(HIDE_PAYWALLED_STORAGE_KEY) === 'true';
} catch {
return false;
}
}
function storeHidePaywalled(hidePaywalled) {
try {
localStorage.setItem(HIDE_PAYWALLED_STORAGE_KEY, String(hidePaywalled));
} catch {
// Persistence is optional; filtering remains available in memory.
}
}
function formatDate(datetimeIso) {
if (!datetimeIso.trim()) {
return '';
}
const date = new Date(datetimeIso);
return Number.isNaN(date.getTime()) ? '' : dateFormatter.format(date);
}
function extractTeaser(card) {
const link = card.querySelector('a[data-component="TeaserLinkContainer"]');
const imageContainer = card.querySelector('a-img[src]');
const image = imageContainer?.querySelector('img') ??
card.querySelector('figure[data-component="Image"] img');
const headline = card.querySelector('[data-component="TeaserHeadline"]');
const paywallIcon = headline?.querySelector('img[src*="heise_plus_blue.svg"]');
const synopsis = card.querySelector('[data-component="TeaserSynopsis"]');
const meta = card.querySelector('[data-component="TeaserMeta"]');
const time = meta?.querySelector('time[datetime]');
const branding = meta?.querySelector('[data-component="Branding"]');
const comments = Array.from(meta?.querySelectorAll('span.items-center') ?? [])
.find((element) => element.querySelector('img[src*="comments_"]'));
const commentIcon = comments?.querySelector(
'img[src*="comments_outline_grey_dark.svg"]'
) ?? comments?.querySelector('img[src*="comments_"]');
const url = link?.getAttribute('href') ?? '';
const title = headline?.textContent?.trim() ?? '';
const synopsisText = synopsis?.textContent?.trim() ?? '';
if (!url.trim() || !title || !synopsisText) {
return null;
}
const datetimeIso = time?.getAttribute('datetime') ?? '';
const commentCount = comments?.textContent?.match(/\d[\d.\s]*/)?.[0]
.replace(/\s/g, '') ?? '';
return {
url,
imageUrl: imageContainer?.getAttribute('src') ?? image?.currentSrc ??
image?.getAttribute('src') ?? '',
imageAlt: imageContainer?.getAttribute('alt') ?? image?.getAttribute('alt') ?? '',
imageWidth: imageContainer?.getAttribute('width') ??
image?.getAttribute('width') ?? '',
imageHeight: imageContainer?.getAttribute('height') ??
image?.getAttribute('height') ?? '',
title,
synopsis: synopsisText,
datetimeIso,
displayDate: formatDate(datetimeIso),
commentCount,
commentIconUrl: commentIcon?.getAttribute('src') ?? '',
branding: branding?.textContent?.trim() ?? '',
isPaywalled: Boolean(paywallIcon),
paywallIconUrl: paywallIcon?.getAttribute('src') ?? '',
};
}
function extractCards(section, cardSelector = CARD_SELECTOR) {
return Array.from(section.querySelectorAll(cardSelector))
.map(extractTeaser)
.filter(Boolean);
}
function createIcon(icon) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '16');
svg.setAttribute('height', '16');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
if (icon === 'grid') {
for (const [x, y] of [[3, 3], [14, 3], [3, 14], [14, 14]]) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', String(x));
rect.setAttribute('y', String(y));
rect.setAttribute('width', '7');
rect.setAttribute('height', '7');
svg.append(rect);
}
} else if (icon === 'list') {
for (const y of [5, 12, 19]) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', '4');
line.setAttribute('y1', String(y));
line.setAttribute('x2', '20');
line.setAttribute('y2', String(y));
svg.append(line);
}
} else if (icon === 'close') {
for (const [x1, y1, x2, y2] of [[5, 5, 19, 19], [19, 5, 5, 19]]) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', String(x1));
line.setAttribute('y1', String(y1));
line.setAttribute('x2', String(x2));
line.setAttribute('y2', String(y2));
svg.append(line);
}
} else {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', '12');
circle.setAttribute('cy', '12');
circle.setAttribute('r', '3');
svg.append(circle);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute(
'd',
'M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21H9.6v-.1A1.7 1.7 0 0 0 8.5 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3V9.6h.1A1.7 1.7 0 0 0 4.6 8.5a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.1A1.7 1.7 0 0 0 15.5 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.16.38.39.72.7 1 .3.26.69.4 1.1.4h.1v4h-.1a1.7 1.7 0 0 0-1.8.6Z'
);
svg.append(path);
}
return svg;
}
function createViewButton(view, label, activeView) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'hel-view-button';
button.dataset.view = view;
button.setAttribute('aria-pressed', String(view === activeView));
button.append(createIcon(view));
const text = document.createElement('span');
text.textContent = label;
button.append(text);
return button;
}
function createMediaLink(teaser, highQualityThumbnail = false) {
const link = document.createElement('a');
link.className = 'hel-media-link';
link.setAttribute('href', teaser.url);
const image = document.createElement('a-img');
image.className = 'hel-image';
image.setAttribute('alt', teaser.imageAlt);
image.setAttribute('quality', highQualityThumbnail ? '90' : '75');
image.setAttribute('high-dpi-quality', highQualityThumbnail ? '90' : '70');
if (teaser.imageUrl) {
image.setAttribute('src', teaser.imageUrl);
}
if (teaser.imageWidth) {
image.setAttribute('width', teaser.imageWidth);
}
if (teaser.imageHeight) {
image.setAttribute('height', teaser.imageHeight);
}
link.append(image);
return link;
}
function createHeadline(teaser, featured) {
const heading = document.createElement(featured ? 'h2' : 'h3');
heading.className = 'hel-headline';
const link = document.createElement('a');
link.setAttribute('href', teaser.url);
if (teaser.isPaywalled && teaser.paywallIconUrl) {
const paywallIcon = document.createElement('img');
paywallIcon.className = 'hel-paywall-icon';
paywallIcon.setAttribute('src', teaser.paywallIconUrl);
paywallIcon.setAttribute('alt', 'heise Plus');
link.append(paywallIcon);
}
link.append(document.createTextNode(teaser.title));
heading.append(link);
return heading;
}
function createTextContent(teaser, featured) {
const content = document.createElement('div');
content.className = 'hel-card-content';
content.append(createHeadline(teaser, featured));
const synopsis = document.createElement('p');
synopsis.className = 'hel-synopsis';
synopsis.textContent = teaser.synopsis;
content.append(synopsis);
const meta = document.createElement('div');
meta.className = 'hel-meta';
const time = document.createElement('time');
time.className = 'hel-meta-time';
time.setAttribute('datetime', teaser.datetimeIso);
time.textContent = teaser.displayDate || '\u00a0';
const comments = document.createElement('span');
comments.className = 'hel-meta-comments';
if (teaser.commentCount) {
comments.setAttribute(
'aria-label',
`${teaser.commentCount} ${teaser.commentCount === '1' ? 'Kommentar' : 'Kommentare'}`
);
if (teaser.commentIconUrl) {
const commentIcon = document.createElement('img');
commentIcon.setAttribute('src', teaser.commentIconUrl);
commentIcon.setAttribute('alt', '');
comments.append(commentIcon);
}
const commentCount = document.createElement('span');
commentCount.textContent = teaser.commentCount;
comments.append(commentCount);
} else {
comments.textContent = '\u00a0';
comments.setAttribute('aria-hidden', 'true');
}
const branding = document.createElement('span');
branding.className = 'hel-meta-branding';
branding.textContent = teaser.branding || '\u00a0';
if (!teaser.branding) {
branding.setAttribute('aria-hidden', 'true');
}
meta.append(time, comments, branding);
content.append(meta);
return content;
}
function createCard(teaser, variant, highQualityThumbnail = false) {
const featured = variant === 'featured';
const article = document.createElement('article');
article.className = `hel-card hel-${variant}-card`;
article.append(createMediaLink(teaser, highQualityThumbnail));
article.append(createTextContent(teaser, featured));
return article;
}
function buildView(view, featuredTeasers, mainTeasers) {
const fragment = document.createDocumentFragment();
const isEmpty = view === 'list'
? mainTeasers.length === 0
: featuredTeasers.length === 0 && mainTeasers.length === 0;
if (isEmpty) {
const empty = document.createElement('p');
empty.className = 'hel-empty';
empty.textContent = 'Für diese Auswahl sind keine Artikel vorhanden.';
fragment.append(empty);
return fragment;
}
if (view === 'grid') {
if (featuredTeasers.length > 0) {
const featuredGrid = document.createElement('div');
featuredGrid.className = 'hel-featured-grid';
featuredGrid.dataset.count = String(featuredTeasers.length);
for (const teaser of featuredTeasers) {
featuredGrid.append(createCard(teaser, 'featured'));
}
fragment.append(featuredGrid);
}
if (mainTeasers.length > 0) {
const mainGrid = document.createElement('div');
mainGrid.className = 'hel-main-grid';
for (const teaser of mainTeasers.slice(0, GRID_ARTICLE_LIMIT)) {
mainGrid.append(createCard(teaser, 'grid'));
}
fragment.append(mainGrid);
}
const remainingTeasers = mainTeasers.slice(GRID_ARTICLE_LIMIT);
if (remainingTeasers.length > 0) {
const continuation = document.createElement('div');
continuation.className = 'hel-list hel-grid-continuation';
for (const teaser of remainingTeasers) {
continuation.append(createCard(teaser, 'list'));
}
fragment.append(continuation);
}
} else {
const list = document.createElement('div');
list.className = 'hel-list';
for (const teaser of mainTeasers) {
list.append(createCard(teaser, 'list', true));
}
fragment.append(list);
}
return fragment;
}
function buildLayout(featuredTeasers, mainTeasers) {
let activeView = readStoredView();
const disabledBrandings = readDisabledBrandings();
let hidePaywalled = readHidePaywalled();
const brandings = new Set(
[...featuredTeasers, ...mainTeasers].map((teaser) => teaser.branding)
);
const root = document.createElement('section');
root.id = ROOT_ID;
root.dataset.view = activeView;
root.setAttribute('aria-label', 'Aktuelle Nachrichten');
const toolbar = document.createElement('div');
toolbar.className = 'hel-toolbar';
const switcher = document.createElement('div');
switcher.className = 'hel-view-switcher';
switcher.setAttribute('role', 'group');
switcher.setAttribute('aria-label', 'Darstellung');
const gridButton = createViewButton('grid', 'Raster', activeView);
const listButton = createViewButton('list', 'Liste', activeView);
switcher.append(gridButton, listButton);
toolbar.append(switcher);
const settings = document.createElement('div');
settings.className = 'hel-settings';
const settingsButton = document.createElement('button');
settingsButton.type = 'button';
settingsButton.className = 'hel-settings-button';
settingsButton.setAttribute('aria-expanded', 'false');
settingsButton.setAttribute('aria-controls', 'hel-branding-menu');
settingsButton.append(createIcon('settings'));
const settingsText = document.createElement('span');
settingsText.textContent = 'Einstellungen';
settingsButton.append(settingsText);
const brandingMenu = document.createElement('div');
brandingMenu.id = 'hel-branding-menu';
brandingMenu.className = 'hel-branding-menu';
brandingMenu.setAttribute('role', 'group');
brandingMenu.hidden = true;
const settingsBackdrop = document.createElement('div');
settingsBackdrop.className = 'hel-settings-backdrop';
settingsBackdrop.hidden = true;
settingsBackdrop.setAttribute('aria-hidden', 'true');
const brandingTitle = document.createElement('div');
brandingTitle.id = 'hel-branding-menu-title';
brandingTitle.className = 'hel-branding-menu-title';
brandingTitle.textContent = 'Kategorien anzeigen';
brandingMenu.setAttribute('aria-labelledby', brandingTitle.id);
brandingMenu.append(brandingTitle);
const settingsCloseButton = document.createElement('button');
settingsCloseButton.type = 'button';
settingsCloseButton.className = 'hel-settings-close';
settingsCloseButton.setAttribute('aria-label', 'Einstellungen schließen');
settingsCloseButton.title = 'Einstellungen schließen';
settingsCloseButton.append(createIcon('close'));
brandingMenu.append(settingsCloseButton);
function createBrandingOption(branding, labelText) {
const option = document.createElement('label');
option.className = 'hel-branding-option';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = branding;
checkbox.checked = !disabledBrandings.has(branding);
const label = document.createElement('span');
label.textContent = labelText;
option.append(checkbox, label);
return option;
}
const brandingColumns = [
[
['', 'Newsticker'],
['heise developer', 'heise developer'],
['heise security', 'heise security'],
["c't Magazin", "c't Magazin"],
['iX Magazin', 'iX Magazin'],
['Mac & i Magazin', 'Mac & I Magazin'],
],
[
['Make Magazin', 'Make Magazin'],
['heise+ exklusiv', 'heise+ exklusiv'],
['heise autos', 'heise autos'],
['bestenlisten', 'bestenlisten'],
],
];
for (const columnBrandings of brandingColumns) {
const column = document.createElement('div');
column.className = 'hel-branding-column';
for (const [branding, labelText] of columnBrandings) {
if (brandings.has(branding)) {
column.append(createBrandingOption(branding, labelText));
}
}
brandingMenu.append(column);
}
const paywallOption = document.createElement('label');
paywallOption.className = 'hel-branding-option';
const paywallCheckbox = document.createElement('input');
paywallCheckbox.type = 'checkbox';
paywallCheckbox.checked = !hidePaywalled;
const paywallLabel = document.createElement('span');
paywallLabel.textContent = 'Heise Plus Artikel anzeigen';
paywallOption.append(paywallCheckbox, paywallLabel);
brandingMenu.lastElementChild.append(paywallOption);
settings.append(settingsButton, brandingMenu);
toolbar.append(settings);
root.append(toolbar, settingsBackdrop);
const content = document.createElement('div');
content.className = 'hel-content';
function renderContent() {
const isEnabled = (teaser) =>
!disabledBrandings.has(teaser.branding) &&
(!hidePaywalled || !teaser.isPaywalled);
content.replaceChildren(buildView(
activeView,
featuredTeasers.filter(isEnabled),
mainTeasers.filter(isEnabled)
));
}
function setSettingsOpen(open) {
brandingMenu.hidden = !open;
settingsBackdrop.hidden = !open;
settingsButton.setAttribute('aria-expanded', String(open));
}
renderContent();
root.append(content);
settingsButton.addEventListener('click', () => {
setSettingsOpen(settingsButton.getAttribute('aria-expanded') !== 'true');
});
settingsCloseButton.addEventListener('click', () => {
setSettingsOpen(false);
settingsButton.focus();
});
brandingMenu.addEventListener('change', (event) => {
const checkbox = event.target.closest('input[type="checkbox"]');
if (!checkbox || !brandingMenu.contains(checkbox)) {
return;
}
if (checkbox === paywallCheckbox) {
hidePaywalled = !checkbox.checked;
storeHidePaywalled(hidePaywalled);
renderContent();
return;
}
if (checkbox.checked) {
disabledBrandings.delete(checkbox.value);
} else {
disabledBrandings.add(checkbox.value);
}
storeDisabledBrandings(disabledBrandings);
renderContent();
});
document.addEventListener('click', (event) => {
if (!settings.contains(event.target)) {
setSettingsOpen(false);
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !brandingMenu.hidden) {
setSettingsOpen(false);
settingsButton.focus();
}
});
switcher.addEventListener('click', (event) => {
const button = event.target.closest('button[data-view]');
if (!button || !switcher.contains(button)) {
return;
}
const nextView = button.dataset.view;
if ((nextView !== 'grid' && nextView !== 'list') || nextView === activeView) {
return;
}
activeView = nextView;
root.dataset.view = activeView;
gridButton.setAttribute('aria-pressed', String(activeView === 'grid'));
listButton.setAttribute('aria-pressed', String(activeView === 'list'));
storeView(activeView);
renderContent();
});
return root;
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) {
return;
}
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${ROOT_ID} {
--hel-surface: #ffffff;
--hel-surface-elevated: #f7f8f7;
--hel-text: #181a19;
--hel-text-muted: #5e625f;
--hel-rule: #c9cdca;
--hel-media: #e3e6e4;
--hel-hover-surface: #eceeed;
--hel-overlay: rgb(24 26 25 / 48%);
--hel-synopsis: #000000;
box-sizing: border-box;
width: 100%;
max-width: 1240px;
margin: 0 auto;
padding: 30px 28px 64px;
color: var(--hel-text);
background: var(--hel-surface);
font-family: "Source Sans VF", system-ui, -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
#${ROOT_ID}[data-hel-theme="light"] {
--hel-surface: #ffffff;
--hel-surface-elevated: #f7f8f7;
--hel-text: #181a19;
--hel-text-muted: #5e625f;
--hel-rule: #c9cdca;
--hel-media: #e3e6e4;
--hel-hover-surface: #eceeed;
--hel-overlay: rgb(24 26 25 / 48%);
--hel-synopsis: #000000;
}
#${ROOT_ID}[data-hel-theme="dark"] {
--hel-surface: #323232;
--hel-surface-elevated: #1a1f23;
--hel-text: #edf1f4;
--hel-text-muted: #a8b0b7;
--hel-rule: #3a434b;
--hel-media: #2a3138;
--hel-hover-surface: #252c33;
--hel-overlay: rgb(0 0 0 / 62%);
--hel-synopsis: #dce2e7;
}
#${ROOT_ID} *,
#${ROOT_ID} *::before,
#${ROOT_ID} *::after {
box-sizing: border-box;
}
#${ROOT_ID} .hel-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0;
margin: -20px 0 10px;
}
#${ROOT_ID} .hel-view-switcher {
display: inline-flex;
align-items: stretch;
}
#${ROOT_ID} .hel-view-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 3px;
min-height: 36px;
margin-right: 7px;
padding: 0;
border: 0;
border-bottom: 3px solid transparent;
border-radius: 0;
color: var(--hel-text);
background: transparent;
font: 700 13px/1 Avenir, "Avenir Next", "Helvetica Neue", sans-serif;
letter-spacing: 0;
cursor: pointer;
}
#${ROOT_ID} .hel-view-button[aria-pressed="true"] {
color: var(--brand-branding);
border-bottom-color: var(--brand-branding);
}
#${ROOT_ID}[data-hel-theme="dark"] .hel-view-button[aria-pressed="true"] {
color: #ffffff;
border-bottom-color: #ffffff;
}
#${ROOT_ID} .hel-view-button:hover:not([aria-pressed="true"]) {
color: var(--brand-branding);
}
#${ROOT_ID}[data-hel-theme="dark"] .hel-view-button:hover:not([aria-pressed="true"]) {
color: #ffffff;
}
#${ROOT_ID} .hel-settings-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 36px;
padding: 0 10px;
border: 0;
border-radius: 0;
color: var(--hel-text-muted);
background: transparent;
font: 700 13px/1 Avenir, "Avenir Next", "Helvetica Neue", sans-serif;
letter-spacing: 0;
cursor: pointer;
}
#${ROOT_ID} .hel-settings-button:hover,
#${ROOT_ID} .hel-settings-button[aria-expanded="true"] {
color: var(--hel-text);
background: var(--hel-hover-surface);
}
#${ROOT_ID} .hel-settings {
position: relative;
align-self: stretch;
display: flex;
align-items: center;
}
#${ROOT_ID} .hel-branding-menu {
position: fixed;
z-index: 1001;
top: 50%;
left: 50%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: min(560px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
gap: 8px 18px;
margin: 0;
padding: 24px;
overflow-y: auto;
border: 0;
border-radius: 0;
color: var(--hel-text);
background: var(--hel-surface-elevated);
box-shadow: 0 18px 48px rgb(24 26 25 / 28%);
transform: translate(-50%, -50%);
}
#${ROOT_ID} .hel-branding-menu[hidden] {
display: none;
}
#${ROOT_ID} .hel-settings-backdrop {
position: fixed;
z-index: 1000;
inset: 0;
background: var(--hel-overlay);
}
#${ROOT_ID} .hel-settings-backdrop[hidden] {
display: none;
}
#${ROOT_ID} .hel-branding-menu-title {
grid-column: 1 / -1;
margin: 0 0 4px;
padding: 0;
font: 700 13px/1.2 Avenir, "Avenir Next", "Helvetica Neue", sans-serif;
letter-spacing: 0;
}
#${ROOT_ID} .hel-branding-column {
display: grid;
align-content: start;
gap: 8px;
min-width: 0;
}
#${ROOT_ID} .hel-settings-close {
position: absolute;
top: 12px;
right: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 0;
color: var(--hel-text-muted);
background: transparent;
cursor: pointer;
}
#${ROOT_ID} .hel-settings-close:hover {
color: var(--hel-text);
background: var(--hel-hover-surface);
}
#${ROOT_ID} .hel-branding-option {
display: flex;
align-items: center;
gap: 9px;
min-height: 34px;
padding: 6px 0;
border: 0;
color: var(--hel-text);
background: transparent;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
cursor: pointer;
}
#${ROOT_ID} .hel-branding-option:has(input:checked) {
color: var(--hel-text);
}
#${ROOT_ID} .hel-branding-option input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--brand-branding);
}
#${ROOT_ID} .hel-view-button:focus-visible,
#${ROOT_ID} .hel-settings-button:focus-visible,
#${ROOT_ID} .hel-settings-close:focus-visible,
#${ROOT_ID} .hel-branding-option:has(input:focus-visible),
#${ROOT_ID} a:focus-visible {
outline: 3px solid var(--brand-branding);
outline-offset: 3px;
}
#${ROOT_ID} .hel-content {
min-width: 0;
}
#${ROOT_ID} .hel-featured-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 30px;
margin-bottom: 50px;
}
#${ROOT_ID} .hel-featured-grid[data-count="1"] {
grid-template-columns: minmax(0, 1fr);
}
#${ROOT_ID} .hel-main-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 40px 26px;
}
#${ROOT_ID} .hel-grid-continuation {
margin-top: 44px;
}
#${ROOT_ID} .hel-card {
min-width: 0;
margin: 0;
}
#${ROOT_ID} .hel-media-link {
display: block;
width: 100%;
overflow: hidden;
aspect-ratio: 16 / 10;
border-bottom: 4px solid var(--brand-branding);
background: var(--hel-media);
}
#${ROOT_ID} .hel-featured-card .hel-media-link {
aspect-ratio: 16 / 9;
border-bottom-width: 6px;
}
#${ROOT_ID} .hel-image,
#${ROOT_ID} .hel-image img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
#${ROOT_ID} .hel-image img {
transition: transform 180ms ease;
}
#${ROOT_ID} .hel-media-link:hover .hel-image img {
transform: scale(1.015);
}
#${ROOT_ID} .hel-card-content {
min-width: 0;
padding-top: 14px;
}
#${ROOT_ID} .hel-headline {
margin: 0;
color: var(--hel-text);
font-size: 20px;
font-weight: 600;
line-height: 27.5px;
overflow-wrap: anywhere;
}
#${ROOT_ID} .hel-featured-card .hel-headline {
font-size: 20px;
line-height: 27.5px;
}
#${ROOT_ID} .hel-headline a {
color: inherit;
text-decoration: none;
}
#${ROOT_ID} .hel-paywall-icon {
display: inline-block;
width: auto;
height: 16px;
margin-right: 0.3em;
vertical-align: middle;
transform: translateY(-0.1em);
}
#${ROOT_ID} .hel-headline a:hover {
color: var(--brand-branding);
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
}
#${ROOT_ID}[data-hel-theme="dark"] .hel-headline a:hover {
color: #ffffff;
}
#${ROOT_ID} .hel-synopsis {
margin: 11px 0 0;
color: var(--hel-synopsis);
font-size: 16px;
font-weight: 400;
line-height: 22px;
overflow-wrap: anywhere;
}
#${ROOT_ID} .hel-featured-card .hel-synopsis {
max-width: 62ch;
}
#${ROOT_ID} .hel-meta {
display: flex;
align-items: center;
white-space: nowrap;
min-height: 1.4em;
margin-top: 13px;
color: var(--hel-text-muted);
font-size: 14px;
font-weight: 400;
line-height: 14px;
}
#${ROOT_ID} .hel-meta > [aria-hidden="true"],
#${ROOT_ID} .hel-meta-time[datetime=""] {
display: none;
}
#${ROOT_ID} .hel-meta > :not([aria-hidden="true"]):not(.hel-meta-time[datetime=""])
~ :not([aria-hidden="true"]):not(.hel-meta-time[datetime=""]) {
margin-left: 12px;
}
#${ROOT_ID} .hel-meta-comments {
display: inline-flex;
align-items: center;
gap: 4px;
}
#${ROOT_ID} .hel-meta-comments img {
display: block;
width: 14px;
height: 14px;
}
#${ROOT_ID} .hel-meta-branding {
overflow: hidden;
text-overflow: ellipsis;
}
#${ROOT_ID} .hel-empty {
margin: 48px 0;
color: var(--hel-text-muted);
font-family: Charter, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
font-size: 22px;
}
#${ROOT_ID} .hel-grid-card {
padding-top: 10px;
}
#${ROOT_ID} .hel-list {
}
#${ROOT_ID} .hel-list-card {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 24px;
padding: 22px 0;
border-bottom: 1px solid var(--hel-rule);
}
#${ROOT_ID} .hel-list-card .hel-media-link {
align-self: start;
aspect-ratio: 16 / 10;
border-bottom-width: 4px;
}
#${ROOT_ID} .hel-list-card .hel-card-content {
padding-top: 0;
}
#${ROOT_ID} .hel-list-card .hel-headline {
font-size: 20px;
}
#${ROOT_ID}[data-view="list"] .hel-list {
display: grid;
gap: 20px;
}
#${ROOT_ID}[data-view="list"] .hel-list-card {
grid-template-columns: 96px minmax(0, 1fr);
gap: 20px;
height: 96px;
padding: 0;
border-bottom: 0;
overflow: hidden;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-media-link {
width: 96px;
height: 96px;
aspect-ratio: 1;
border-bottom: 0;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-media-link .hel-image {
width: 144px;
height: 144px;
transform: scale(0.666667);
transform-origin: top left;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-card-content {
display: flex;
flex-direction: column;
height: 96px;
overflow: hidden;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-headline {
display: -webkit-box;
overflow: hidden;
font-size: 20px;
line-height: 27.5px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-synopsis {
overflow: hidden;
margin-top: 4px;
line-height: 22px;
text-overflow: ellipsis;
}
@media (max-width: 1099px) {
#${ROOT_ID} .hel-main-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 699px) {
#${ROOT_ID} {
padding: 20px 16px 44px;
}
#${ROOT_ID} .hel-toolbar {
align-items: stretch;
}
#${ROOT_ID} .hel-view-button {
min-width: 68px;
padding: 0 8px;
}
#${ROOT_ID} .hel-settings-button {
padding: 0 8px;
}
#${ROOT_ID} .hel-settings-button span {
display: none;
}
#${ROOT_ID} .hel-featured-grid,
#${ROOT_ID} .hel-main-grid {
grid-template-columns: minmax(0, 1fr);
}
#${ROOT_ID} .hel-featured-grid {
gap: 34px;
margin-bottom: 42px;
}
#${ROOT_ID} .hel-main-grid {
gap: 34px;
}
#${ROOT_ID} .hel-list-card {
grid-template-columns: minmax(0, 1fr);
gap: 14px;
padding: 22px 0 26px;
}
#${ROOT_ID}[data-view="list"] .hel-list-card {
grid-template-columns: 64px minmax(0, 1fr);
gap: 12px;
height: auto;
min-height: 64px;
padding: 0;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-media-link {
width: 64px;
height: 64px;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-media-link .hel-image {
width: 96px;
height: 96px;
}
#${ROOT_ID}[data-view="list"] .hel-list-card .hel-card-content {
height: auto;
min-height: 64px;
}
#${ROOT_ID} .hel-meta {
overflow: hidden;
}
}
@media (prefers-reduced-motion: reduce) {
#${ROOT_ID} .hel-image img {
transition: none;
}
}
`;
document.head.append(style);
}
function cleanupInitialization() {
observer?.disconnect();
observer = null;
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (retryFrameId !== null) {
cancelAnimationFrame(retryFrameId);
retryFrameId = null;
}
}
function attemptMount() {
if (mounted || document.getElementById(ROOT_ID)) {
mounted = true;
cleanupInitialization();
return true;
}
const target = document.querySelector(TARGET_SELECTOR);
const featuredSection = document.querySelector(FEATURED_SELECTOR);
const mainSections = Array.from(document.querySelectorAll(MAIN_SELECTOR));
if (!target || !featuredSection || mainSections.length === 0) {
const missing = [
!target && 'target container',
!featuredSection && 'featured section',
mainSections.length === 0 && 'main sections',
].filter(Boolean);
lastFailure = `missing ${missing.join(', ')}`;
return false;
}
if (!target.contains(featuredSection) ||
mainSections.some((section) => !target.contains(section))) {
lastFailure = 'selected source sections were outside the target container';
return false;
}
const featuredTeasers = extractCards(featuredSection).slice(0, 2);
const mainTeasers = mainSections.flatMap((section) =>
extractCards(section, MAIN_LIST_CARD_SELECTOR)
);
if (featuredTeasers.length === 0 || mainTeasers.length === 0) {
lastFailure = `invalid source data (featured: ${featuredTeasers.length}, main: ${mainTeasers.length})`;
return false;
}
try {
const root = buildLayout(featuredTeasers, mainTeasers);
injectStyles();
target.replaceChildren(root);
setupThemeSync(root);
mounted = true;
cleanupInitialization();
return true;
} catch (error) {
lastFailure = `rendering failed: ${error instanceof Error ? error.message : String(error)}`;
return false;
}
}
function scheduleRetry() {
if (mounted || retryFrameId !== null) {
return;
}
retryFrameId = requestAnimationFrame(() => {
retryFrameId = null;
attemptMount();
});
}
if (attemptMount()) {
return;
}
observer = new MutationObserver(scheduleRetry);
observer.observe(document.body, { childList: true, subtree: true });
timeoutId = setTimeout(() => {
if (mounted) {
return;
}
cleanupInitialization();
console.warn(`[Heise Editorial Layout] Mount aborted after 5 seconds: ${lastFailure}.`);
}, OBSERVER_TIMEOUT_MS);
})();