% 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 here and loads lum-euler for maths. % Both are left to the document instead: this template ships OpenSans and % RobotoMono, and lum-euler is not packaged in Debian's TeX Live. The document % is expected to \setsansfont, \setmonofont and load a Unicode maths font % before using anything below. \headerfont is what the theme uses for titles % and other display text; redefine it if you want titles in a second face. \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}% } % -- Palette. Four colours drive everything the theme draws. Override any of % them in the preamble, before or after loading trantor: % % \definecolor{accent}{HTML}{BF1238} % \colorlet{TrantorDark}{makopurple1} % % accent progress bars and emphasis % TrantorDark header band, section dividers, standout panels, frame numbers % TrantorLight text on dark, and anything that has to read against it % TrantorMuted affiliations, footlines, arxiv tags. This is the 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}} % -- Header bar carrying the frame title (hidden on plain pages). % The band is a rule of height (header height - headsep) with depth headsep, so % headsep is the only thing that puts space under the title. geometry owns it. \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}% } % 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, which ltx-talk does not provide. Frames after it keep counting % but are left out of the total, so the denominator is the last frame of the % talk proper. This is what appendixnumberbeamer does for beamer. The count is % carried through the aux file, so it settles on the second run. \newif\iftrantorAppendix \trantorAppendixfalse \newcommand\trantorMainFrames{\@totalframes} % Appendix frames are numbered among themselves, as appendixnumberbeamer does, % but by subtracting an offset rather than resetting the counter. The class % records a property per frame keyed on that counter, so resetting it collides % with ltx-talk's own frame. labels and breaks \ref to a frame. Properly % this belongs in the class, which could renumber and relabel together. \NewDocumentCommand \appendix { } {% \global\trantorAppendixtrue \protected@write\@auxout{}{% \string\gdef\string\trantorMainFrames{\the\value{frame}}}% } % Numerator and denominator for the frame counter and the progress bars. These % are count registers rather than macros because pgfmath expands its argument % while parsing, and a macro carrying a conditional sends it into a loop. \newcount\trantor@n \newcount\trantor@d \newcommand\trantor@setcounts{% \iftrantorAppendix \trantor@n=\numexpr\value{frame}-\trantorMainFrames\relax \trantor@d=\numexpr\@totalframes-\trantorMainFrames\relax \else \trantor@n=\value{frame}\relax \trantor@d=\numexpr\trantorMainFrames\relax \fi } \newcommand\trantorFrameNumber{\trantor@setcounts\the\trantor@n} \newcommand\trantorTotalFrames{\trantor@setcounts\the\trantor@d} \newcommand\trantor@setratio{% \ifnum\trantor@d>0 \pgfmathsetmacro{\trantorratio}{min(\the\trantor@n / \the\trantor@d, 1)}% \else \def\trantorratio{0}% \fi } % -- Bottom progress bar + frame counter (hidden on plain pages). \newif\iftrantorPlainPage \trantorPlainPagefalse \AddToHook{shipout/foreground}{% \iftrantorPlainPage\else \trantorArtifactBegin \begin{tikzpicture}[remember picture, overlay] \trantor@setcounts \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=\scriptsize, text=TrantorDark] at (current page.south east) {\the\trantor@n\,/\,\the\trantor@d}; \end{tikzpicture}% \trantorArtifactEnd \fi \global\trantorPlainPagefalse } % -- Remember the current section title, and dim the others in the mini-TOC. \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 at each new section so % that a stale subsection 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 } } } % Soften the dimming of inactive TOC entries on the section page (0.2 -> 0.7). % ltx-talk exposes no key for this yet, so we override the class-private % \__talk_toc_dest:w. Revisit if a supported interface appears or the internal % changes. \cs_set_protected:Npn \__talk_toc_dest:w #1 . #2 . #3 . #4 . #5 \q_stop #6 { \bool_lazy_or:nnTF { \bool_lazy_and_p:nn { \int_compare_p:nNn { \value { section } } = {#2} } { \int_compare_p:nNn { \value { subsection } } = 0 } } { \bool_lazy_and_p:nn { \int_compare_p:nNn { \value { section } } = {#2} } { \int_compare_p:nNn { \value { subsection } } = {#3} } } {#6} { \opacity_begin:n { 0.7 } #6 \opacity_end: } } \ExplSyntaxOff % -- 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}{% \global\trantorPlainPagetrue \begin{frame}{}% \thispagestyle{plain}% \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}% \vspace{0.12\paperheight}% \end{frame}% } % -- Divider pages, following metropolis: on the ordinary light background, a % block set in the middle of the slide holding the section title over a % progress bar, and for a subsection the subsection title beneath that. The % bar is the same progress the footer bar shows, drawn at half width. The % footer bar and frame number stay, so these are not plain pages in the sense % the shipout hook means. \newlength\trantor@barwidth \newlength\trantor@barfill % \trantorProgressBar{}: filled to the current position, grey after. \NewDocumentCommand \trantorProgressBar { m } {% \setlength\trantor@barwidth{#1}% \trantor@setcounts \trantor@setratio \setlength\trantor@barfill{\trantorratio\trantor@barwidth}% \textcolor{TrantorDark}{\rule{\trantor@barfill}{0.6pt}}% \textcolor{TrantorDark!20}{% \rule{\dimexpr\trantor@barwidth-\trantor@barfill\relax}{0.6pt}}% } \NewDocumentCommand \trantor@divider { m } {% \begin{frame}{}% \thispagestyle{plain}% \vfill \centering \begin{minipage}{0.52\linewidth}% \raggedright {\headerfont\Large\bfseries\trantorSectionTitle\par}% \vspace{0.4em}% \trantorProgressBar{\linewidth}% \IfBlankF{#1}{% \par\vspace{0.8em}% {\headerfont\large\bfseries #1\par}% }% \end{minipage}% \par \vfill \end{frame}% } \NewDocumentCommand \sectionpage { } { \trantor@divider {} } \NewDocumentCommand \subsectionpage { } { \trantor@divider {\trantorSubsectionTitle} } % \standout{...}: a full frame with the same dark panel as \sectionpage, % and the given text centred on it. \NewDocumentCommand \standout { +m } {% \global\trantorPlainPagetrue \begin{frame}{}% \thispagestyle{plain}% \noindent \begin{tikzpicture} \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}{0.8\linewidth} \centering \headerfont\LARGE\bfseries #1 \end{minipage}% }; \end{tikzpicture}% \end{frame}% } \makeatother