Heise Editorial Layout

Grid and list homepage reading surface for heise.de

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);
})();