% trantor.sty -- a Metropolis-style theme for the ltx-talk class. % LuaLaTeX only. % % Copyright (c) 2026 Marco Pompili, https://github.com/mrc-pop/trantor % Received under the MIT license and redistributed here, with modifications, % under the GNU GPL version 3 or later. See COPYING. \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{trantor}[2026-08-01 v1.0 Metropolis-like theme for ltx-talk] % LuaTeX only (XeTeX is unsupported and the fonts need a Unicode engine). \RequirePackage{iftex} \RequireLuaTeX \RequirePackage{tikz} \RequirePackage{pifont} % -- Fonts. Upstream sets Libertinus and loads lum-euler; both are left to the % document, which must \setsansfont, \setmonofont and load a maths font. \renewcommand{\familydefault}{\sfdefault} \linespread{1.18} \providecommand\headerfont{\sffamily} \makeatletter % Tighten the space around display maths -- the default 11pt reads as a big gap % on a slide, especially inside a list. Re-applied on every \normalsize so the % size files don't reset it. \g@addto@macro\normalsize{% \setlength\abovedisplayskip{5pt plus 2pt minus 1pt}% \setlength\belowdisplayskip{5pt plus 2pt minus 1pt}% \setlength\abovedisplayshortskip{5pt plus 2pt minus 1pt}% \setlength\belowdisplayshortskip{5pt plus 2pt minus 1pt}% } % -- Colours. Set your own accent anywhere in the preamble with, e.g., % \definecolor{accent}{HTML}{BF1238} % \definecolor{TrantorDark}{HTML}{23373B} % Four colour palette: % 1. accent progress bars, title rule % 2. TrantorDark headings, section dividers, standout panels, frame numbers % 3. TrantorLight text set against TrantorDark % 4. TrantorMuted affiliation, footline, arxiv. Lightest grey that still meets % WCAG AA contrast (4.5:1) on white (~4.54:1). \providecolor{accent}{HTML}{2A7AE2} \providecolor{TrantorDark}{HTML}{000000} \providecolor{TrantorLight}{HTML}{FFFFFF} \providecolor{TrantorMuted}{HTML}{767676} \AddToHook{begindocument}{\colorlet{TrantorAccent}{accent}} % itemize/enumerate labels in the body colour rather than the class default \AddToHook{begindocument}{\colorlet{structure}{TrantorDark}} % \alert in the accent colour rather than the class's red, as metropolis % coloured alerted text from the theme palette \AddToHook{begindocument}{\colorlet{alert}{TrantorAccent}} % beamer set quotations in italic; ported decks expect it \AddToHook{env/quote/begin}{\itshape} \AddToHook{env/quotation/begin}{\itshape} % -- Header bar carrying the frame title (hidden on plain pages). % headsep is what pads under the title; the height key alone only pads above. \geometry{headsep=10pt} \AddToHook{begindocument}{% \EditInstance{header}{std}{ background-color = TrantorDark, color = TrantorLight, print-frame-title = true, height = 40pt, font = \headerfont\bfseries, left-hspace = \dimexpr\Gm@lmargin - 0.6cm\relax, }% \EditInstance{footer}{std}{element-order = {}}% \EditInstance{frametitle}{header}{after-vspace = 6mm minus 3mm}% } % -- A frame with no \frametitle gets no header band. ltx-talk draws it % unconditionally, which leaves an empty dark slab across the top of every % full-bleed image slide. metropolis omitted the bar on untitled frames and % decks are written expecting that. \trantorEmptyHeadertrue restores the bar. % % The band is drawn from \@oddhead, so this replaces \ps@talk; \pagestyle has % to be re-issued for the new definition to take. \newif\iftrantorEmptyHeader \trantorEmptyHeaderfalse \ExplSyntaxOn \cs_new_protected:Npn \trantor@header { \iftrantorEmptyHeader \UseInstance { header } { std } \else % Guard the class internal: a class change should not make this an error. \cs_if_exist:NTF \g__talk_frame_title_tl { \tl_if_empty:NF \g__talk_frame_title_tl { \UseInstance { header } { std } } } { \UseInstance { header } { std } } \fi } \cs_set_nopar:Npn \ps@talk { \cs_set_nopar:Npn \@oddhead { \trantor@header \hfil } \cs_set_nopar:Npn \@oddfoot { \UseInstance { footer } { std } } \cs_set_eq:NN \@evenhead \@oddhead \cs_set_eq:NN \@evenfoot \@oddfoot } \ExplSyntaxOff \pagestyle{talk} % Like \frametitle, but forces a gap below the title on packed slides where the % header's own spacing collapses. The \par gets us into vertical mode first. \NewDocumentCommand\stitle{m}{\frametitle{#1}\par\addvspace{4mm}} % \standoutline{...}: a centred line of text in the title's sans/size/colour, % for emphasis within a normal (light) frame, e.g. closing a slide. \NewDocumentCommand\standoutline{m}{% \par \begingroup \centering\headerfont\Large\bfseries\color{TrantorDark}#1\par \endgroup } % Tick and cross, for marking things right or wrong. \NewDocumentCommand\yes{}{\ding{51}} \NewDocumentCommand\no{}{\ding{55}} % \footline{...}: a small reference or citation pinned to the bottom-left of % the slide. Overlaid, so it can go anywhere inside the frame. \NewDocumentCommand\footline{m}{% \begin{tikzpicture}[remember picture, overlay] \node[anchor=south west, xshift=1ex, yshift=0.7ex, font=\scriptsize, text=TrantorMuted] at (current page.south west) {#1}; \end{tikzpicture}% } % \arxiv{1306.5001}: a small, grey "arXiv:" tag linked to the abstract. % Meant to sit inline next to a statement, e.g. a theorem. \NewDocumentCommand\arxiv{m}{% \href{https://arxiv.org/abs/#1}{{\headerfont\scriptsize\color{TrantorMuted}arXiv:#1}}% } % -- \appendix: frames after it are numbered among themselves and left out of % the total. The count travels through the aux file, so it settles on the % second run. % ltx-talk gained its own \appendix upstream after 0.3.12; once a release % ships it, revisit this in favour of the class's version. \newif\iftrantorAppendix \trantorAppendixfalse \newcommand\trantorMainFrames{\@totalframes} \NewDocumentCommand \appendix { } {% \global\trantorAppendixtrue \protected@write\@auxout{}{% \string\gdef\string\trantorMainFrames{\the\value{frame}}}% \protected@write\@auxout{}{% \string\gdef\string\trantorMainPanels{\the\value{trantorpanels}}}% } % -- Panels (standout pages) stay out of the frame numbering, as under % metropolis, which set noframenumbering on standout frames. The class's frame % counter cannot be touched -- notes and property records key on it -- so the % panels are counted separately and subtracted for display. The totals travel % through the aux file like \trantorMainFrames does. \newcounter{trantorpanels} \newcounter{trantorpanellast} \newcommand\trantorTotalPanels{0} \newcommand\trantorMainPanels{\trantorTotalPanels} % \immediate: a deferred write would wait for a shipout that never comes. \AtEndDocument{% \immediate\write\@auxout{% \string\gdef\string\trantorTotalPanels{\the\value{trantorpanels}}}% } % Count each panel frame once, though the body may run once per slide. \newcommand\trantor@countpanel{% \ifnum\value{trantorpanellast}=\value{frame}\else \stepcounter{trantorpanels}% \stepcounter and \setcounter act globally \setcounter{trantorpanellast}{\value{frame}}% \fi } % Count registers, not macros: pgfmath expands while parsing, and a macro % carrying a conditional loops. \newcount\trantor@n \newcount\trantor@d \newcommand\trantor@setcounts{% \iftrantorAppendix \trantor@n=\numexpr\value{frame}-\trantorMainFrames -\value{trantorpanels}+\trantorMainPanels\relax \trantor@d=\numexpr\@totalframes-\trantorMainFrames -\trantorTotalPanels+\trantorMainPanels\relax \else \trantor@n=\numexpr\value{frame}-\value{trantorpanels}\relax \trantor@d=\numexpr\trantorMainFrames-\trantorMainPanels\relax \fi } % Fraction of the way through the talk, shared by the footer bar and % \trantorProgressBar. The zero test must stay outside pgfmath, which % evaluates both arms of a ternary. \newcommand\trantor@setratio{% \trantor@setcounts \ifnum\trantor@d>0 \pgfmathsetmacro{\trantorratio}{min(\the\trantor@n / \the\trantor@d, 1)}% \else \def\trantorratio{0}% \fi } % \trantorProgressBar{}: a rule of the given width, filled to the % current position and grey the rest of the way. \newlength\trantor@barwidth \newlength\trantor@barfill \NewDocumentCommand \trantorProgressBar { m } {% \setlength\trantor@barwidth{#1}% \trantor@setratio % dimen = , so \trantorratio scales the width \setlength\trantor@barfill{\trantorratio\trantor@barwidth}% \textcolor{TrantorAccent}{\rule{\trantor@barfill}{0.6pt}}% \textcolor{TrantorDark!20}{% \rule{\dimexpr\trantor@barwidth-\trantor@barfill\relax}{0.6pt}}% } % -- Bottom progress bar + frame counter (hidden on plain pages). \newif\iftrantorPlainPage \trantorPlainPagefalse \AddToHook{shipout/foreground}{% \iftrantorPlainPage\else \trantorArtifactBegin \begin{tikzpicture}[remember picture, overlay] \trantor@setratio \fill[TrantorAccent] (current page.south west) rectangle ([xshift=\trantorratio\paperwidth, yshift=1pt] current page.south west); \node[anchor=south east, inner sep=5pt, font=\tiny, text=TrantorDark] at (current page.south east) {\the\trantor@n\,/\,\the\trantor@d}; \end{tikzpicture}% \trantorArtifactEnd \fi \global\trantorPlainPagefalse } % -- Remember the section and subsection titles, for the dividers. \ExplSyntaxOn \tl_new:N \g__trantor_section_tl \AddToHook{section/begin}{ \cs_if_exist:NT \l__talk_section_tl { \tl_gset:NV \g__trantor_section_tl \l__talk_section_tl } } \cs_new_protected:Npn \__trantor_print_section: { \tl_use:N \g__trantor_section_tl } \cs_set_eq:NN \trantorSectionTitle \__trantor_print_section: % Likewise the subsection, for \subsectionpage. Cleared when a new section % starts so a stale title cannot leak into the next one. \tl_new:N \g__trantor_subsection_tl \AddToHook{section/begin}{ \tl_gclear:N \g__trantor_subsection_tl } \AddToHook{subsection/begin}{ \cs_if_exist:NT \l__talk_subsection_tl { \tl_gset:NV \g__trantor_subsection_tl \l__talk_subsection_tl } } \cs_new_protected:Npn \__trantor_print_subsection: { \tl_use:N \g__trantor_subsection_tl } \cs_set_eq:NN \trantorSubsectionTitle \__trantor_print_subsection: % Suspend tagging around the theme's decorative / pagination graphics when % tagging is on (a no-op otherwise), so the progress bar and frame counter stay % out of the structure tree instead of being flagged as loose content. \cs_new_protected:Npn \trantorArtifactBegin { \cs_if_exist:NT \tag_suspend:n { \tag_suspend:n { trantor } } } \cs_new_protected:Npn \trantorArtifactEnd { \cs_if_exist:NT \tag_resume:n { \tag_resume:n { trantor } } } \ExplSyntaxOff % Run #1 inside the current frame if there is one, else inside a fresh frame. % Mirrors the class's \maketitle, so a document can wrap any of these in its % own frame and add content alongside -- a \note, or \footline for a revision % stamp -- following the beamer idiom. \ExplSyntaxOn \cs_new_protected:Npn \trantor@inframe #1 { \bool_if:NTF \l__talk_frame_bool {#1} { \begin { frame } #1 \end { frame } } } \ExplSyntaxOff % \plainpage: suppress the footer progress bar and frame number on the current % frame's pages, as the title, standout, and divider pages do. Use it at the % top of a frame, e.g. one holding a full-bleed image. \NewDocumentCommand\plainpage{}{% \global\trantorPlainPagetrue \thispagestyle{plain}% } % -- Title page. Drop a logo.png next to your slides, or redefine \titlelogo. \newcommand{\titlelogo}{% \IfFileExists{logo.png} {\includegraphics[height=1.4cm, alt={Organisation logo}]{logo.png}}{}% } \renewcommand{\maketitle}{% \trantor@inframe{% \plainpage \vfill \hspace*{0.04\paperwidth}% \begin{minipage}{0.82\paperwidth} \raggedright {\color{TrantorDark}\headerfont\Large\bfseries \@title\par}% \ifx\@subtitle\empty\else \vspace{0.35em}% {\color{TrantorDark}\headerfont\large \@subtitle\par}% \fi \vspace{0.5em}% {\color{TrantorAccent}\rule{\linewidth}{0.8pt}\par}% \vspace{1.3em}% % Metadata on the left, logo on the right; bottom-aligned so the logo sits % level with the date and grows upward rather than below it. \begin{minipage}[b]{0.66\linewidth} \raggedright {\normalsize \@author\par}% \ifx\@institute\empty\else \vspace{0.2em}% {\small\color{TrantorMuted} \@institute\par}% \fi \vspace{0.2em}% {\small \@date\par}% \end{minipage}% \hfill \begin{minipage}[b]{0.30\linewidth} \raggedleft \titlelogo \end{minipage}% \end{minipage}% \hspace*{0.04\paperwidth}% % shrinkable, so a long author block compresses this space instead of % overfilling the fixed frame height \vspace{0.12\paperheight minus 0.08\paperheight}% }% } % -- A full-bleed dark panel with the given body centred in a minipage of the % given width. \NewDocumentCommand \trantor@panel { m +m } {% \trantor@inframe{% \plainpage \trantor@countpanel \noindent % overlay=false explicitly: the panel is positioned from its bounding box, % so it breaks if the document sets \tikzstyle{every picture}+=[overlay], % which decks ported from beamer commonly do. \begin{tikzpicture}[overlay=false, remember picture=false] \useasboundingbox (0,0) rectangle (\linewidth,-\textheight); \fill[TrantorDark] (-\Gm@lmargin, \paperheight) rectangle (\dimexpr\paperwidth-\Gm@lmargin\relax, -\paperheight); \node[text=TrantorLight, anchor=center, inner sep=0pt] at (0.5\linewidth, -0.5\textheight) {% \begin{minipage}{#1} \centering #2 \end{minipage}% }; \end{tikzpicture}% }% } % -- Divider pages: the section title over a progress bar, set on the ordinary % light background. Not plain pages: the footer bar and frame number stay. % Like the class's \maketitle, the dividers work inside a frame the document % opened itself, so beamer's \begin{frame}\sectionpage\note{...}\end{frame} % idiom carries over. Called between frames, they open their own. \NewDocumentCommand \trantor@divider { +m } {% \trantor@inframe{% \thispagestyle{plain}% \vfill \centering \begin{minipage}{0.52\linewidth}% \raggedright {\color{TrantorDark}\headerfont\Large\bfseries\trantorSectionTitle\par}% \vspace{0.4em}% \trantorProgressBar{\linewidth}% #1% \end{minipage}% \par \vfill }% } \NewDocumentCommand \sectionpage { } { \trantor@divider{} } % The subsection divider is the same, naming the subsection under the bar. \NewDocumentCommand \subsectionpage { } {% \trantor@divider{% \par\vspace{0.8em}% {\color{TrantorDark}\headerfont\large\bfseries\trantorSubsectionTitle\par}% }% } % \standout{...}: a full-bleed dark panel with the given text centred on it. \NewDocumentCommand \standout { +m } {% \trantor@panel{0.8\linewidth}{\headerfont\LARGE\bfseries #1}% } \makeatother