% Modified for the LaTeX.to template catalogue, not the original file. The changes are listed in changes.txt.
%!TEX root=template.tex

%% ----------------------------------------------------------------------------
%%  novathesis — nt-core.sty
%% ----------------------------------------------------------------------------
%%
%% Version 8.4.0 (2026-09-27)
%% Copyright (C) 2004-26 by João M. Lourenço <joao.lourenco@fct.unl.pt>
%% Departamento de Informática (www.di.fct.unl.pt)
%% Faculdade de Ciências e Tecnologia (www.fct.unl.pt)
%% Universidade NOVA de Lisboa (www.unl.pt)
%%
%% Core engine: expl3 utilities, data management, template options, macros, person/file management
%% ----------------------------------------------------------------------------

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% RESOURCES & ENGINES (Expl3 & UTILITIES)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Convenience alias: executes #1 for XeTeX/LuaTeX, #2 for pdfTeX
\newcommand{\ifxeorlua}[2]{\iftutex#1\else#2\fi}

% --------------------------------------------------------
% Change main font size on the fly
% --------------------------------------------------------
\newcommand{\setmemoirfontsize}[1]{\input{mem#1.clo}}


% --------------------------------------------------------
% Date and month
% --------------------------------------------------------
% \babelmonthname{<1..12>} → localized month name (current babel language)
\newcommand{\babelmonthname}[1]{%
  \csname month\romannumeral\numexpr#1\relax name\endcsname
}



\ExplSyntaxOn
% Robust titlecase function: modifies the TL variable in-place
\tl_new:N \l__nt_titlecase_head_tl
\tl_new:N \l__nt_titlecase_tail_tl
\cs_new_protected:Nn \nt_titlecase_first:N
 {
   \tl_if_empty:NF #1
    {
      \tl_set:Ne \l__nt_titlecase_head_tl { \tl_head:N #1 }
      \tl_set:Ne \l__nt_titlecase_tail_tl { \tl_tail:N #1 }
      \tl_set:Ne #1
        { \exp_not:N \MakeUppercase { \l__nt_titlecase_head_tl }
          \l__nt_titlecase_tail_tl }
    }
 }

\tl_new:N \l__nt_titlecase_work_tl
\DeclareDocumentCommand {\MakeTitlecase} { m }
  {
    \tl_set:Ne \l__nt_titlecase_work_tl { #1 }
    \nt_titlecase_first:N \l__nt_titlecase_work_tl
    \tl_use:N \l__nt_titlecase_work_tl
  }

% ── \IfBeginsWithAnyOf{text}{prefix_list}{true}{false} ────────────────

% 1. Create Variables
\int_new:N \l__nt_check_len_int
\str_new:N \l__nt_slice_str
\tl_new:N \l__nt_clean_input_tl
\bool_new:N \l__nt_match_found_bool

% 2. Generate the missing variant
%    This forces \str_range to accept an expanded ('e') first argument.
\cs_generate_variant:Nn \str_range:nnn { enn }

\NewDocumentCommand { \IfBeginsWithAnyOf } { m m +m +m }
 {
  % Step A: Store and clean the input string
  \tl_set:Nn \l__nt_clean_input_tl { #1 }
  \tl_trim_spaces:N \l__nt_clean_input_tl
  
  \bool_set_false:N \l__nt_match_found_bool
  
  % Step B: Iterate through prefixes
  \clist_map_inline:nn { #2 }
   {
    % Get length of the current prefix (##1)
    \int_set:Nn \l__nt_check_len_int { \str_count:n { ##1 } }
    
    % Step C: Extract substring
    % We use the generated ':enn' variant to expand \l__nt_clean_input_tl
    % into actual text (e.g., "Apple") BEFORE slicing.
    \str_set:Nx \l__nt_slice_str 
      { 
        \str_range:enn { \l__nt_clean_input_tl } { 1 } { \l__nt_check_len_int } 
      }
    
    % Step D: Compare
    \exp_args:No \str_if_eq:nnTF \l__nt_slice_str { ##1 }
     {
      \bool_set_true:N \l__nt_match_found_bool
      \clist_map_break: 
     }
     { }
   }
   
  \bool_if:NTF \l__nt_match_found_bool { #3 } { #4 }
 }




% ── \AddValidProcessor{key}{engines} / \ValidateLtxProcessor{u}{s}{d} ─

% ---------------------------------------------------------
% 1. Variables and Data Storage
% ---------------------------------------------------------

% Store the rules (Univ/School -> Allowed Engines)
\prop_new:N \g_novathesis_proc_rules_prop

% Store the current engine name (pdf, lua, or xe)
\str_new:N \g_novathesis_current_engine_str
\tl_new:N  \l_novathesis_found_rule_tl
\bool_new:N \l_novathesis_rule_found_bool

% Detect the current engine immediately
\sys_if_engine_pdftex:T
  { \str_gset:Nn \g_novathesis_current_engine_str { pdf } }
\sys_if_engine_luatex:T
  { \str_gset:Nn \g_novathesis_current_engine_str { lua } }
\sys_if_engine_xetex:T
  { \str_gset:Nn \g_novathesis_current_engine_str { xe } }

% 3. Define the user-facing command
\NewDocumentCommand \NTengineName {}
  {
    % Use \str_use:N to retrieve the content of the variable
    \str_use:N \g_novathesis_current_engine_str \hologo { LaTeX }
  }

% ---------------------------------------------------------
% 2. User Interface to Add Rules (replacement for \validprocessor)
% ---------------------------------------------------------

% Syntax: \AddValidProcessor{key}{list,of,engines}
\NewDocumentCommand { \AddValidProcessor } { m m }
  {
    \prop_gput:Nnn \g_novathesis_proc_rules_prop { #1 } { #2 }
  }

% Reusable engine-in-clist test used below by both \ValidateLtxProcessor
% and \NTRequireEngine. \clist_if_in:N.../:n... comparisons against
% \g_novathesis_current_engine_str (a str variable) do not work: str
% variables store their content with every character forced to catcode 12
% ("other"), and \clist_if_in's own comparison is catcode-sensitive, so it
% never matches a clist built from ordinary document-mode text (normal
% catcodes) even when the two are visibly the same string -- confirmed by
% direct testing, including \tl_set:Ne-copying the str into a plain tl
% first (still fails, since the catcode-12 tokens are what gets copied).
% \str_if_eq:VnT IS str-aware (compares by character code, ignoring
% catcode), so looping the clist and testing each item that way is what
% actually works.
\bool_new:N \l__nt_engineok_bool
\cs_new_protected:Npn \__nt_check_engine_in_clist:N #1
  {
    \bool_set_false:N \l__nt_engineok_bool
    \clist_map_inline:Nn #1
      {
        \str_if_eq:VnT \g_novathesis_current_engine_str { ##1 }
          { \bool_set_true:N \l__nt_engineok_bool \clist_map_break: }
      }
  }

% ---------------------------------------------------------
% 3. The Main Validator
% ---------------------------------------------------------

% Message definition for the error
\msg_new:nnnn { novathesis } { invalid-processor }
  { Invalid~processor~for~"#1". }
  {
    You~are~using~\g_novathesis_current_engine_str~(la)tex,~
    but~"#1"~only~allows:~#2.
  }

% Generate the "NeNTF" variant used below to look up hierarchy keys built
% from expanded macro arguments (#1/#2/#3 etc.). Guarded with \cs_if_exist:NF
% since some l3kernel releases may already provide this variant natively;
% calling \cs_generate_variant:Nn on an already-existing name is not safe to
% assume harmless across kernel versions.
\cs_if_exist:NF \prop_get:NeNTF
  { \cs_generate_variant:Nn \prop_get:NnNTF { NeNTF } }

\NewDocumentCommand { \ValidateLtxProcessor } { m m m }
  {
    % #1 = Univ, #2 = School, #3 = Degree

    % Reset flags
    \bool_set_false:N \l_novathesis_rule_found_bool

    % -----------------------------------------------------
    % A. Hierarchy Lookup (Univ/Sch/Deg -> Univ/Sch -> Univ)
    % -----------------------------------------------------

    % 1. Check Univ/School/Degree
    \prop_get:NeNTF \g_novathesis_proc_rules_prop { #1/#2/#3 }
      \l_novathesis_found_rule_tl
      { \bool_set_true:N \l_novathesis_rule_found_bool }
      {
        % 2. Check Univ/School
        \prop_get:NeNTF \g_novathesis_proc_rules_prop { #1/#2 }
          \l_novathesis_found_rule_tl
          { \bool_set_true:N \l_novathesis_rule_found_bool }
          {
            % 3. Check Univ -- :NeNTF (expanding), NOT :NnNTF: #1 here is
            % the literal token \@UNIV (macro arguments aren't expanded by
            % default), and levels 1/2 above already correctly use the
            % expanding "e" variant to turn it into e.g. "uminho" before
            % searching the prop. Using the non-expanding "n" variant here
            % searched for the key "\@UNIV" itself, which never matches
            % anything \AddValidProcessor ever registers (plain, already-
            % literal strings like "uminho") -- so any school relying on
            % this fallback (Univ-only rule, no School/Degree-specific
            % one) silently never got validated at all.
            \prop_get:NeNTF \g_novathesis_proc_rules_prop { #1 }
              \l_novathesis_found_rule_tl
              { \bool_set_true:N \l_novathesis_rule_found_bool }
              {
                % No rule found: Do nothing (Implicitly Valid)
              }
          }
      }

    % -----------------------------------------------------
    % B. Validation Logic
    % -----------------------------------------------------

    \bool_if:NT \l_novathesis_rule_found_bool
      {
        % Check if the current engine is in the allowed list
        \__nt_check_engine_in_clist:N \l_novathesis_found_rule_tl
        \bool_if:NF \l__nt_engineok_bool
          {
            % ERROR: Engine not found in allowed list
            \msg_error:nnxx { novathesis } { invalid-processor }
              { #1/#2/#3 }
              { \clist_use:Nn \l_novathesis_found_rule_tl { ,~ } }
          }
      }
  }

% ---------------------------------------------------------
% 4. Helper: Print current processor name (The \pdfprocessor equivalent)
% ---------------------------------------------------------
\NewDocumentCommand { \PrintProcessorName } { }
  {
    \str_case:Vn \g_novathesis_current_engine_str
      {
        { pdf } { \hologo{pdfLaTeX} }
        { lua } { \hologo{luaLaTeX} }
        { xe }  { \hologo{XeLaTeX} }
      }
  }

% ── \AddValidDegree{key}{degrees} / \ValidateDegree{u}{s}{d} ───────────
% Same hierarchy-lookup shape as \AddValidProcessor/\ValidateLtxProcessor
% above (Univ/School/Degree -> Univ/School -> Univ, looked up with the
% expanding "e" prop-key variant for the same reason noted on
% \ValidateLtxProcessor), but for which doctypes ("degrees") a school
% offers rather than which (La)TeX engine it needs. Two doctype-vocabulary
% rules are baked in here, rather than requiring every declaration to
% spell every variant out:
%   - "phd" also covers phdplan/phdprop, and "msc" also covers mscplan --
%     proposal/plan phases of the same degree, not a separate programme;
%   - doctype=plain (no specific degree) is never restricted -- it is a
%     generic fallback, not a school-specific programme.

\prop_new:N \g_novathesis_degree_rules_prop
\tl_new:N  \l_novathesis_found_degree_tl
\bool_new:N \l_novathesis_degree_rule_found_bool

% Syntax: \AddValidDegree{key}{list,of,doctypes}
\NewDocumentCommand { \AddValidDegree } { m m }
  {
    \prop_gput:Nnn \g_novathesis_degree_rules_prop { #1 } { #2 }
  }

% Expands the family shorthands above into \l__nt_degree_expanded_clist,
% checked instead of the raw declaration; error messages still quote the
% raw declaration (#1), so what's displayed matches what was written.
\clist_new:N \l__nt_degree_expanded_clist
\cs_new_protected:Npn \__nt_expand_degree_families:N #1
  {
    \clist_set:NV \l__nt_degree_expanded_clist #1
    \clist_if_in:NnT #1 { phd }
      { \clist_put_right:Nn \l__nt_degree_expanded_clist { phdplan, phdprop } }
    \clist_if_in:NnT #1 { msc }
      { \clist_put_right:Nn \l__nt_degree_expanded_clist { mscplan } }
  }

% Same manual clist_map_inline + str_if_eq loop \__nt_check_engine_in_clist:N
% uses above, for the same reason: catcode-12 str-variable comparisons are
% unreliable with \clist_if_in. Not actually at risk here (neither side is
% a \str_new:N variable), but kept consistent with its sibling check.
\bool_new:N \l__nt_degreeok_bool
\cs_new_protected:Npn \__nt_check_degree_in_clist:N #1
  {
    \__nt_expand_degree_families:N #1
    \bool_set_false:N \l__nt_degreeok_bool
    \clist_map_inline:Nn \l__nt_degree_expanded_clist
      {
        \str_if_eq:eeT { ##1 } { \option{/novathesis/doctype} }
          { \bool_set_true:N \l__nt_degreeok_bool \clist_map_break: }
      }
  }

\msg_new:nnnn { novathesis } { invalid-degree }
  { Invalid~degree~for~"#1". }
  {
    You~selected~doctype=\option{/novathesis/doctype},~
    but~"#1"~only~offers:~#2.
  }

\NewDocumentCommand { \ValidateDegree } { m m m }
  {
    % #1 = Univ, #2 = School, #3 = Degree (school-variant, or the current
    % doctype when the school string had no third segment -- same value
    % \ValidateLtxProcessor's own third argument already is at the call
    % site in nt-setup.sty, reused here unchanged)

    \bool_set_false:N \l_novathesis_degree_rule_found_bool

    \str_if_eq:eeTF { \option{/novathesis/doctype} } { plain }
      { } % plain is never restricted -- see the note above
      {
        % A. Hierarchy Lookup (Univ/Sch/Deg -> Univ/Sch -> Univ)
        \prop_get:NeNTF \g_novathesis_degree_rules_prop { #1/#2/#3 }
          \l_novathesis_found_degree_tl
          { \bool_set_true:N \l_novathesis_degree_rule_found_bool }
          {
            \prop_get:NeNTF \g_novathesis_degree_rules_prop { #1/#2 }
              \l_novathesis_found_degree_tl
              { \bool_set_true:N \l_novathesis_degree_rule_found_bool }
              {
                \prop_get:NeNTF \g_novathesis_degree_rules_prop { #1 }
                  \l_novathesis_found_degree_tl
                  { \bool_set_true:N \l_novathesis_degree_rule_found_bool }
                  {
                    % No rule found: Do nothing (Implicitly Valid)
                  }
              }
          }

        % B. Validation Logic
        \bool_if:NT \l_novathesis_degree_rule_found_bool
          {
            \__nt_check_degree_in_clist:N \l_novathesis_found_degree_tl
            \bool_if:NF \l__nt_degreeok_bool
              {
                \msg_error:nnxx { novathesis } { invalid-degree }
                  { #1/#2/#3 }
                  { \clist_use:Nn \l_novathesis_found_degree_tl { ,~ } }
              }
          }
      }
  }

% ---------------------------------------------------------
% 5. Engine requirement for individual FontStyles/*.sty files
% ---------------------------------------------------------
% \AddValidProcessor/\ValidateLtxProcessor above are keyed by
% Univ/School/Degree and run ONCE, early, at school-selection time
% (nt-setup.sty) -- before style/font is even resolved. They can't catch
% a font-specific requirement: a school's bundled default font may need
% XeTeX/LuaTeX even though the school itself has no such restriction (the
% user may freely \ntsetup{style/font=...} to something pdfLaTeX-
% compatible instead). Several FontStyles/*.sty files instead grew their
% own ad hoc "\ifxeorlua{}{\ClassError{...}}" guard, each with different
% wording, and four more (arial/calibri/futura/verdana) call fontspec
% commands with no guard at all -- under pdfLaTeX, \setmainfont doesn't
% even exist (fontspec isn't loaded), so the failure is a raw "Undefined
% control sequence" instead of a clear message.
%
% \NTRequireEngine{<label>}{<comma-list-of-allowed-engines>}, called from
% the font file itself at the point it actually needs XeTeX/LuaTeX, gives
% every one of them the same clear, standardized message, reusing the
% existing "invalid-processor" wording above and the same str-aware
% engine check \ValidateLtxProcessor uses (see \__nt_check_engine_in_clist:N
% above).
\clist_new:N \l__nt_requireengine_clist
\NewDocumentCommand { \NTRequireEngine } { m m }
  {
    \clist_set:Nn \l__nt_requireengine_clist {#2}
    \__nt_check_engine_in_clist:N \l__nt_requireengine_clist
    \bool_if:NF \l__nt_engineok_bool
      {
        \msg_error:nnxx { novathesis } { invalid-processor }
          {#1}
          { \clist_use:Nn \l__nt_requireengine_clist { ,~ } }
      }
  }




% ── File existence utilities ───────────────────────────────────────────
% \IfFileExistsInDirsTF, \IfFileExistsCase, \IfFontExistsCase,
% \InputIfFileExistsInDirsTF — starred (*) processes all matches;
% non-starred stops on first success

% Each file-search command below uses its OWN found-flag, so that nesting one
% inside another's action code cannot clobber the outer result (they used to
% share a single \l_file_found_bool).  \IfFileExistsInDirsTF sets its flag
% globally to escape the \group_begin:/\group_end: that scopes its scratch
% variables; the others set their flags locally.
\bool_new:N \g__nt_fileindirs_found_bool
\bool_new:N \l__nt_filecase_found_bool
\bool_new:N \l__nt_inputindirs_found_bool
% Scratch variables for \IfFileExistsInDirsTF, declared once (previously
% \tl_new was called inside the command body, which errored on the 2nd call).
\tl_new:N \l__nt_fileindirs_dir_tl
\tl_new:N \l__nt_fileindirs_path_tl
\tl_new:N \l__nt_fileindirs_lastchar_tl

\NewDocumentCommand { \IfFileExistsInDirsTF } { s O { } m m m m }
 {
  \bool_gset_false:N \g__nt_fileindirs_found_bool
  \group_begin:
    \clist_map_inline:nn { #4 }
     {
      \tl_set:Nn \l__nt_fileindirs_dir_tl { ##1 }
      \tl_trim_spaces:N \l__nt_fileindirs_dir_tl

      % Smart Slash Logic
      \tl_if_empty:NF \l__nt_fileindirs_dir_tl
       {
        \tl_set:Nx \l__nt_fileindirs_lastchar_tl
          { \tl_item:Nn \l__nt_fileindirs_dir_tl { -1 } }
        \str_if_eq:VnF \l__nt_fileindirs_lastchar_tl { / }
         { \tl_put_right:Nn \l__nt_fileindirs_dir_tl { / } }
       }

      % CONSTRUCT FULL PATH: Prefix + Dir + Filename
      \tl_set:Nx \l__nt_fileindirs_path_tl { #2 \l__nt_fileindirs_dir_tl #3 }

      \file_if_exist:nT { \l__nt_fileindirs_path_tl }
       {
        \bool_gset_true:N \g__nt_fileindirs_found_bool
        \xdef\ntfoundpath{ \l__nt_fileindirs_path_tl }
        #5
        \IfBooleanF { #1 } { \clist_map_break: }
       }
     }
  \group_end:

  \bool_if:NF \g__nt_fileindirs_found_bool { #6 }
 }


% \IfFileExistsCase*[search-path]{ {file1}{action1}, {file2}{action2} }{ default-action }
% * (Optional): logic defined in the underlying expl3 code.
% #2 (Optional): A specific directory or path to prepend to the filenames.
% #3 (Mandatory): A comma-separated list of pairs formatted as {filename}{code to run if found}.
% #4 (Mandatory): The "Else" branch. This code runs only if none of the files in your list are found.
\NewDocumentCommand { \IfFileExistsCase } { s O { } m m }
  {
    \bool_set_false:N \l__nt_filecase_found_bool
    \clist_map_inline:nn { #3 }
      {
        % ##1 is {filepath}{action}
        \__filecase_split_pair:nnnn { #1 } { #2 } ##1
      }
    % Else branch
    \bool_if:NF \l__nt_filecase_found_bool { #4 }
  }

% #1: Starred status
% #2: Prefix
% #3: Filename
% #4: Action
% Own scratch variables rather than the kernel's shared \l_tmpa_tl/\l_tmpb_tl:
% the action code (#4) typically \input{}s the file just found, and that file
% may re-enter \IfFileExistsCase, which would clobber a shared scratch.
\tl_new:N \l__nt_filecase_path_tl
\tl_new:N \l__nt_filecase_noext_tl

\cs_new_protected:Npn \__filecase_split_pair:nnnn #1 #2 #3 #4
  {
    % 1. Construct the full path
    \tl_set:Nn \l__nt_filecase_path_tl { #2 #3 }

    \file_if_exist:nT { \l__nt_filecase_path_tl }
      {
        \bool_set_true:N \l__nt_filecase_found_bool

        \tl_set_eq:NN \l__nt_filecase_noext_tl \l__nt_filecase_path_tl

        % Regex: Match a dot, followed by non-dots and non-slashes, until end of string.
        % This ensures we don't accidentally match a dot in a directory name (e.g. "ver.1/img").
        \regex_replace_once:nnN { \.[^./]*\Z } { } \l__nt_filecase_noext_tl

        \xdef\ntfoundpath{ \l__nt_filecase_path_tl }
        \xdef\ntfoundpathnoext{ \l__nt_filecase_noext_tl }
        #4
        
        % Break if not starred
        \bool_if:NF #1 { \clist_map_break: }
      }
  }


% Own found-flag rather than the kernel's shared scratch boolean: the action
% code (#3) runs inside the loop and may itself use that shared scratch, which
% would corrupt the "no font found" test performed after the loop.
\bool_new:N \l__nt_fontcase_found_bool

\NewDocumentCommand { \IfFontExistsCase } { s m m }
  {
    \bool_set_false:N \l__nt_fontcase_found_bool
    \clist_map_inline:nn { #2 }
      {
        % Check if the command \IfFontExistsTF exists (provided by fontspec)
        \cs_if_exist:cTF { IfFontExistsTF }
          {
            \IfFontExistsTF { ##1 }
              {
                \bool_set_true:N \l__nt_fontcase_found_bool
                % Execute the code #3, passing the found font name (##1)
                #3 { ##1 }
                % If starred, break the loop after the first match
                \IfBooleanT { #1 } { \clist_map_break: }
              }
              { } % Do nothing if font doesn't exist
          }
          {
            % Fallback if fontspec isn't loaded or engine is pdfLaTeX
            \typeout { novathesis:~fontspec~not~loaded.%
              ~Skipping~font~check~for~##1. }
          }
      }
    
    % Optional: warning if NO fonts from the list were found
    \bool_if:NF \l__nt_fontcase_found_bool
      {
        \typeout
          { novathesis:~Warning:~No~fonts~from~the~list~{ #2 }~were~found. }
      }
  }


% --------------------------------------------------------
% Input the file from the first dir list (if any)
% #1 = file name
% #2 = list of directories
% #3 = code if succeed [OPTIONAL]
% #4 = code if fails [OPTIONAL]

% 1. Create variables
\NewDocumentCommand { \InputIfFileExistsInDirsTF } { m m +m +m }
  {
    \bool_set_false:N \l__nt_inputindirs_found_bool

    % Iterate through directories (#2)
    \clist_map_inline:nn { #2 }
      {
        % Check if 'directory/filename' exists
        \file_if_exist:nTF { ##1 / #1 }
          {
            % A. Input the file
            \file_input:n { ##1 / #1 }

            % B. Execute Success Code (#3)
            #3

            % C. Mark found and break loop
            \bool_set_true:N \l__nt_inputindirs_found_bool
            \clist_map_break:
          }
          {}
      }

    % Handle 'Not Found' case
    \bool_if:NF \l__nt_inputindirs_found_bool
      { #4 }
  }




% Default bookmark level for \ChapterNoTitle items.
% Set to 1 inside \ntprint{frontmatter} so they nest under the Front Matter node.
\def\@nt@bkm@level { 0 }

% 1. Define the internal logic (accepts the argument as-is)
\cs_new_protected:Nn \__user_chapter_logic:n
 {
  % Note: Inside ExplSyntaxOn, spaces are ignored, so we don't need % at EOL
  \clearforchapter
  
  % Create the anchor. 
  % #1 is now the fully expanded string (e.g., "Project Alpha").
  \phantomsection%
  \bookmark[level=\@nt@bkm@level, dest=\HyperLocalCurrentHref] { #1 }
  
  % Reset spacing/formatting
  \noindent \ignorespaces
 }


% 2. Generate the variant that forces expansion ('e' = expand fully)
\cs_generate_variant:Nn \__user_chapter_logic:n { e }

% 3. User Interface
\NewDocumentCommand { \ChapterNoTitle } { m }
 {
  % Call the expanded variant.
  % This turns \ChapterNoTitle{\myMacro} into \__user_chapter_logic:n {Expanded Value}
  \__user_chapter_logic:e { #1 }
 }

\clist_new:N \l__nt_uniquify_clist
\NewDocumentCommand { \UniquifyList } { m o }
  {
    \clist_set:Ne \l__nt_uniquify_clist { #1 }
    \clist_remove_duplicates:N \l__nt_uniquify_clist
    \IfValueTF { #2 }
      { \tl_set:Ne #2 { \clist_use:Nn \l__nt_uniquify_clist { , } } }
      { \clist_use:Nn \l__nt_uniquify_clist { , } }
  }


% ── Abstract order accumulator ────────────────────────────────────────
% abstractorder={lang=order-list} (or abstractorder={order-list}) entries
% are collected in \g__nt_abstractorder_seq and applied once by
% \@setabstractorder (called from nt-setup.sty after options are frozen).
\seq_new:N \g__nt_abstractorder_seq
\tl_new:N  \l__nt_ao_lang_tl
\tl_new:N  \l__nt_ao_val_tl
\seq_new:N \l__nt_ao_parts_seq
\cs_new_protected:Npn \@setabstractorder
  {
    \seq_map_inline:Nn \g__nt_abstractorder_seq
      {
        % ##1 is "lang=order-list"  or just "order-list" (main lang implied)
        \seq_set_split:Nnn \l__nt_ao_parts_seq { = } { ##1 }
        \int_compare:nNnTF { \seq_count:N \l__nt_ao_parts_seq } > { 1 }
          {
            \tl_set:Ne \l__nt_ao_lang_tl { \seq_item:Nn \l__nt_ao_parts_seq { 1 } }
            \tl_set:Ne \l__nt_ao_val_tl  { \seq_item:Nn \l__nt_ao_parts_seq { 2 } }
          }
          {
            \tl_set:Ne \l__nt_ao_lang_tl { \@LANG@MAIN }
            \tl_set:Nn \l__nt_ao_val_tl  { ##1 }
          }
        \MemSet * { AbstractOrder } { \l__nt_ao_lang_tl } { \l__nt_ao_val_tl }
      }
  }
\cs_new_protected:Npn \@addtoabstractorder #1
  {
    \seq_gput_right:Nn \g__nt_abstractorder_seq { #1 }
  }

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% DATA MANAGEMENT (MEMORY)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% --------------------------------------------------------
% Search if a data/memory is defined and returns value


% Define a boolean to track if a match was found inside the loop
\bool_new:N \l__memory_match_found_bool

% Command: \datamatchtf
% Syntax:  \datamatchtf{\match}{MEMORYDATA}(list,of,keys){TRUE}{FALSE}
%
% #1: The macro to store the result (\match)
% #2: The memory data container name (MEMORYDATA)
% #3: The list of keys to search (in parentheses)
% #4: True branch code
% #5: False branch code

% \datamatchtf reads memstore's property lists (g__mem_<store>_prop) directly
% and so relies on memstore.sty's variants, in particular \prop_if_in:ceT,
% which memstore generates from the \prop_if_in:NnT base.  memstore is loaded
% by nt-packages.sty, i.e. before this file, so the variant is available here.
% Kept as a safety net in case that load order ever changes; a no-op otherwise.
\cs_if_exist:NF \prop_if_in:ceT
  { \cs_generate_variant:Nn \prop_if_in:NnT { ceT } }

\NewDocumentCommand { \datamatchtf } { s m m r() +m +m }
  {
    % Reset the found boolean to false at the start
    \bool_set_false:N \l__memory_match_found_bool

    % Initialize #2 to empty.  Done ONCE here rather than on every iteration:
    % the loop breaks as soon as a key matches, so re-clearing inside the loop
    % only repeated work for each non-matching key without changing the result
    % (no match -> #2 stays empty; match -> #2 is overwritten just below).
    \IfBooleanTF{#1}
      {
        % STARRED: #2 is a NAME (e.g., "myVar").
        % Use \tl_set:ce to construct the csname from the string.
        \tl_set:ce { #2 } { }
      }
      {
        % NON-STARRED: #2 is a MACRO (e.g., \myVar).
        % Use \tl_set:Ne to set it directly.
        \tl_set:Ne #2 { }
      }

    % Iterate through the comma-separated list of keys (#4 now, due to argument shift).
    \exp_args:Ne \clist_map_inline:nn { #4 }
      {
        % Look up key ##1 in the memstore property list for store #3
        \prop_if_in:ceT { g__mem_#3_prop } { ##1 }
          {
            % If the key exists in the store:
            % 1. Set the user's variable (#2) to the stored value.
            
            \IfBooleanTF{#1}
              { 
                % STARRED: #2 is a NAME (e.g., "myVar"). 
                % Use \tl_set:cn to construct the csname from the string.
                \tl_set:ce { #2 } { \prop_item:ce { g__mem_#3_prop } { ##1 } } 
              }
              { 
                % NON-STARRED: #2 is a MACRO (e.g., \myVar). 
                % Use \tl_set:Nn to set it directly.
                \tl_set:Ne #2 { \prop_item:ce { g__mem_#3_prop } { ##1 } } 
              }

            % 2. Mark that we found a match
            \bool_set_true:N \l__memory_match_found_bool

            % 3. Break the loop immediately
            \clist_map_break:
          }
      }

    % Execute the appropriate branch based on whether a match was found
    % True branch is #5, False branch is #6
    \bool_if:NTF \l__memory_match_found_bool
      { #5 } % Match found
      { #6 } % No match found
  }

\NewDocumentCommand { \datamatch } { s m m r() }
  {
    % Argument mapping:
    % #1 = Star (boolean)
    % #2 = Macro to set / Name of macro (if starred)
    % #3 = Data ID (memory context)
    % #4 = List of keys (inside parens)

    \IfBooleanTF{#1}
      { 
        % Starred version: call \datamatchtf*
        \datamatchtf* { #2 } { #3 } ( #4 )
      }
      { 
        % Non-starred version: call \datamatchtf
        \datamatchtf  { #2 } { #3 } ( #4 ) 
      }
      % --- True Branch (Match Found) ---
      { } 
      % --- False Branch (No Match) ---
      { 
        % Note: In expl3, spaces are ignored, so we use ~ for spaces in text.
        \ClassError { novathesis }
          { Could~not~find~' #3 '~defined~with~any~of~' ( #4 ) '! }
          { }
      }
  }

\NewDocumentCommand { \datamatchcs } { s m m r() }
  {
    \IfBooleanTF{#1}
    {
      % Using expl3 syntax to expand the first argument into a csname
      \exp_args:NNc \datamatch * { #2 } { #3 } ( #4 )
    }
    {
      % Using expl3 syntax to expand the first argument into a csname
      \exp_args:Nc \datamatch { #2 } { #3 } ( #4 )
    }

  }

% --------------------------------------------------------
% Canonical key-hierarchy generator for \datamatch(tf) chains
%
% \nthierkeys{<prefix>}{<suffix>}
%
% Fully expandable.  Expands to the canonical 6-key fallback tier
% (most specific first):
%
%   { <prefix> \@DOCTYPE  , \@DEGREEACR , <suffix> } ,
%   { <prefix> \@DOCCLASS , \@DEGREEACR , <suffix> } ,
%   { <prefix> \@DEGREEACR , <suffix> } ,
%   { <prefix> \@DOCTYPE  , <suffix> } ,
%   { <prefix> \@DOCCLASS , <suffix> } ,
%   { <prefix> <suffix> }
%
% <prefix> must be empty or end with a comma (e.g. "cover,").
% <suffix> is the trailing key part (e.g. "1-1,textcolor" or "textcolor").
%
% Cover lookups use two tiers, cover-specific first, so that any
% per-cover setting always beats any global (degree-wide) one:
%
%   \datamatchtf { \tl } { ThesisCover }
%     ( \nthierkeys{}{1-1,bgcolor} , \nthierkeys{}{bgcolor} ) {...}{...}
%
% Designed for use inside the e-expanded key lists of \datamatch,
% \datamatchtf, and \datamatchcs, but works in any e/x expansion context.
\cs_new:Npn \nthierkeys #1 #2
  {
    { #1 \@DOCTYPE  , \@DEGREEACR , #2 } ,
    { #1 \@DOCCLASS , \@DEGREEACR , #2 } ,
    { #1 \@DEGREEACR , #2 } ,
    { #1 \@DOCTYPE  , #2 } ,
    { #1 \@DOCCLASS , #2 } ,
    { #1 #2 }
  }

\ExplSyntaxOff


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% TEMPLATE OPTIONS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Every option below is declared via the \NTOptNew* helpers in
% nt-options.sty (an internal, l3keys-backed replacement for the external
% `options` package -- see that file's header for the full rationale).
% There is no more "/novathesis/.new family" root declaration: the old
% package's family concept doesn't exist here, since every key lives
% under a single flat l3keys module ("novathesis"); \NTOptNew* already
% bakes the "/novathesis/" prefix into each option's \option{}-mirroring
% csname, so declarations below use the SAME short, relative key names
% the old file already used inside that family context (e.g. "doctype",
% not "/novathesis/doctype").
%
% Shared allowed-language list for lang/all (a list) and lang/main,
% lang/cover, lang/copyright (choices that must accept the same values).
\def\@nt@alllangs{cat,cz,de,dk,en,es,fr,gr,it,nl,pl,pt,sk,uk}

% DOCUMENT
\NTOptNewChoice{doctype}{phd, phdprop, phdplan, msc, mscplan, bsc, plain}{phd}
\NTOptNewChoice{docstatus}{unset, working, provisional, final}{unset} % if not defined by user, unset will be replaced by "working"
\NTOptNewChoice{media}{screen, paper}{screen}
\NTOptNewChoice{spine/layout}{unset, trim, false}{unset}
% Kept for source-compatibility with any \ntsetup{spine/width=...} still
% out there (e.g. nova-fct-cbbi-defaults.clo), but this key is already
% dead in practice: nothing reads \option{/novathesis/spine/width}
% anywhere, since nt-spine.sty declares its OWN, independent l3keys key
% also named "spine/width" (under the same "novathesis" module -- see
% \NTSetup in nt-spine.sty), which is what actually drives spine
% rendering and loads after this file, so it wins. Neither this file nor
% the old options.sty-based one before it ever bridged the two.
\NTOptNewLength{spine/width}{0pt}
\NTOptNewCmd{abstractorder}{\@addtoabstractorder{#1}}
\NTOptNewChoice{abstract/title}{none, before, after}{none} % where to print the work title in abstracts
\NTOptNewChoice{abstract/title/align}{left, center, right}{center} % alignment of the default title block
\NTOptNewValue{schoolgiven}{}
\NTOptNewCmd{school}
  {
    \optionsalso{/novathesis/schoolgiven=#1}%
    \StrCut{#1}{/}\@UNIV\@SCHL\StrCut{\@SCHL}{/}\@SCHL\@DEGREEACR
    \IfStrEq{\@DEGREEACR}{}{\def\@DEGREEACR{\option{/novathesis/doctype}}}{}
  }
\ntsetup{school=nova/fct}
% STYLES
\NTOptNewValue{style/chapter}{bar}
\NTOptNewValue{style/font}{newpx}
\NTOptNewValue{style/font/attributes}{}
\NTOptNewCmd{style/url}{\AfterPreamble{\urlstyle{#1}}}
\NTOptNewCmd{color/links}
  {
    \@ifpackageloaded{hyperref}%
      {\hypersetup{allcolors=#1}}%
      {\PassOptionsToPackage{allcolors=#1}{hyperref}}%
  }
\NTOptNewCmd{color/glossaries/gls}
  {
    \AtEndPreamble{%
      \IfStrEq{#1}{None}{%
        \let\@glslink\glsplainhyperlink%
      }{%
        \renewcommand*{\glstextformat}[1]{\textcolor{#1}{##1}}%
      }%
    }%
  }
% LANGUAGES
\NTOptNewList{lang/extra}{}
\NTOptNewList{lang/all}{\@nt@alllangs}
\NTOptNewChoice{lang/main}{\@nt@alllangs}{en}
\NTOptNewChoice{lang/cover}{\@nt@alllangs}{en}
\NTOptNewChoice{lang/copyright}{\@nt@alllangs}{en}
\NTOptNewCmd{lang}
  {
    \optionsalso{/novathesis/lang/main=#1,
                 /novathesis/lang/cover=#1,
                 /novathesis/lang/copyright=#1}%
  }
\ntsetup{lang=en}
% PRINT ON/OFF
\NTOptNewValue{print/aidisclosure}{aidisclose} % use '7-aidisclose.tex' + 'aidisclose' package
\NTOptNewToggle{print/committee}{false}
\NTOptNewToggle{print/adviser}{true}    % fake option - adviser should always be printed
\NTOptNewValue{print/otherlists}{frontmatter}  % print other lists (lof, lot, loa, lol, …) in the front matter
\NTOptNewValue{print/glossaries}{frontmatter}  % print glossaries in the front matter
% Skip individual "list of ..." entries without commenting out lines in
% 0-Config/6_list_of.tex. Names are whatever \ntaddlistof registers
% (listoffigures, listoftables, listofalgorithms, listoflistings, or any
% custom name) plus "glossaries", special-cased to force
% print/glossaries=false instead (see \myprintglossaries in nt-setup.sty) --
% there being no \ntaddlistof entry for glossaries to skip in the first place.
\NTOptNewList{listof/skip}{}
\NTOptNewToggle{print/frontpage}{false}
\NTOptNewToggle{print/copyright}{true}
\NTOptNewToggle{print/timestamp}{true}
\NTOptNewToggle{print/index}{false}
\NTOptNewToggle{print/colophon}{true}   % print a Colophon page in the back matter
\NTOptNewValue{print/webography}{}      % empty means do not make a separate webography
\NTOptNewToggle{print/statement}{false}
% Prefix "Appendix"/"Annex" to the chapter number in appendix/annex headings
% too, not just in the PDF bookmarks (which already show it by default).
\NTOptNewToggle{print/appendixname}{false}
\NTOptNewList{print/sdgs/list}{}      % {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17}
% The one choice in this codebase whose stored value differs from its
% user-facing name -- see the comment on \NTOptNewChoice in nt-options.sty.
\ExplSyntaxOn
\keys_define:nn { novathesis }
  {
    print/sdgs/type .choice: ,
    print/sdgs/type / inverted .code:n = { \cs_set:cpn { nt@opt@/novathesis/print/sdgs/type } {i} } ,
    print/sdgs/type / normal   .code:n = { \cs_set:cpn { nt@opt@/novathesis/print/sdgs/type } {n} } ,
    print/sdgs/type / mono     .code:n = { \cs_set:cpn { nt@opt@/novathesis/print/sdgs/type } {m} } ,
    print/sdgs/type .initial:n = { inverted } ,
  }
\ExplSyntaxOff
\NTOptNewDim{print/sdgs/size}{2cm}      % any measure
\NTOptNewDim{print/sdgs/hspace}{5mm}    % any measure
\NTOptNewDim{print/sdgs/vspace}{4mm}    % any measure
% CITING THE TEMPLATE
\NTOptNewToggle{cite/template}{true}  % add 'novathesis-manual' to the bibliography
% MISC
\NTOptNewToggle{glossaries/list/reverse}{true}
\NTOptNewValue{glossaries/layout}{lX}
% Per-type letter-group headings ("A", "B", …) before each alphabetic group.
% A list of glossary type names (glossary, acronym, symbols, or any custom
% type added via \ntaddfile{glossaries}[<type>]{...}); off for every type
% by default. Multiple types need an extra pair of braces to protect the
% comma from \ntsetup's own top-level comma split, e.g.
%   \ntsetup{glossaries/groupheadings={acronym,symbols}}
\NTOptNewList{glossaries/groupheadings}{}
% Per-type title override, e.g. rename "Symbols" to "List of Symbols":
%   \ntsetup{glossaries/titles={symbols=List of Symbols}}
% "type=Title" pairs, comma-separated; braces required around the whole
% value for the same reason as glossaries/groupheadings above. Neither the
% type name nor the title text may contain a literal "," or "=" (the parser
% is a simple split, not a full tokenizer).
\NTOptNewValue{glossaries/titles}{}
\NTOptNewToggle{tocintoc}{true}
\NTOptNewToggle{pdfa}{true}  % auto-enable PDF/A-2b when docstatus=final
% NUMBERING
\NTOptNewChoice{numbering/floats}{chapter, sequential}{chapter}
% DEBUG
\NTOptNewList{debug}{}         % cover, index, spine
\NTOptNewValue{debug/color/grid}{orange}   % \debuggrid's auto-triggered color (debug=cover)


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% GENERAL PURPOSE MACROS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% --------------------------------------------------------
% The novathesis logo
% --------------------------------------------------------
% 1. Define the box register
\newsavebox{\novathesisbox}

% 2. Save the image into the box (do this once)
% We calculate the height based on the current font at the time of definition
\sbox{\novathesisbox}{%
  \includegraphics[height=1.2\fontcharht\font`A, vshift=-0.75pt]{\DIRIMAGES/novathesis-text}%
}

% 3. Define the macros to use the box
\def\novathesisimg{\usebox{\novathesisbox}}
\def\novathesistxt{\textbf{nova}thesis}

% 4. Select the output method
% Use \providebox or similar if needed, but \newcommand works here
\iftutex
  \newcommand{\novathesis}{\novathesisimg}
\else
  \newcommand{\novathesis}{\novathesistxt}
\fi


% --------------------------------------------------------
% Define something (except counters) if undefined
% --------------------------------------------------------
\newcommand*{\defifundef}[2]{\ifundef{#1}{#2{#1}}{}}

% --------------------------------------------------------
% Variant of \@for with an optional final argument, that is
% executed iif the loop is not interrupted
% --------------------------------------------------------
% The outer \ntfor/\ntfor@i shells stay classical: their parameter text
% uses ":=" and "[...]" as literal delimiters that must keep catcode 12
% to match call sites written in normal-catcode context, so they cannot
% be defined while ExplSyntaxOn is active. Only the loop body itself
% (no delimiters, no document text) moves to expl3, via a plain-named
% (no "_"/":") auxiliary that classical code can call from anywhere.
\ExplSyntaxOn
\bool_new:N \l__nt_for_break_bool
\cs_new_protected:Npn \@nt@ntforloop #1#2#3#4
  { % #1=loop variable  #2=comma list  #3=per-iteration code  #4=else-branch
    \bool_set_false:N \l__nt_for_break_bool
    \clist_map_inline:nn {#2} { \xdef #1 {##1} #3 }
    \bool_if:NF \l__nt_for_break_bool {#4}
  }
\cs_new:Npn \ntforbreak
  { \bool_set_true:N \l__nt_for_break_bool \clist_map_break: }
\ExplSyntaxOff

\long\def\ntfor#1:=#2\do#3{\@ifnextchar[{\ntfor@i#1:=#2\do#3}{\ntfor@i#1:=#2\do#3[]}}
\long\def\ntfor@i#1:=#2\do#3[#4]{\@nt@ntforloop{#1}{#2}{#3}{#4}}

% --------------------------------------------------------
% ISO 8601 date string: YYYY-MM-DD of \today
% --------------------------------------------------------
\newcommand{\todayiso}{%
  \the\year-%
  \ifnum\month<10 0\fi\the\month-%
  \ifnum\day<10 0\fi\the\day%
}



\ExplSyntaxOn

% --------------------------------------------------------
% Automatically download necessary fonts if using lualatex
% --------------------------------------------------------

\NewDocumentCommand { \ntcheckfonts } {
    O { ttf }
    m
    m
    O { https://raw.githubusercontent.com/%
      joaomlourenco/novathesis-extras/main/Fonts }
    O { \DIRFONTSTYLES/Fonts } }
{
  % #1 = format (ttf)
  % #2 = font name (Calibri)
  % #3 = variants (Regular,Italic,Bold,BoldItalic)
  % #4 = base URL
  % #5 = target folder

  % Check if using XeLaTeX or LuaLaTeX
  \ifxeorlua
  {
    \nt_check_fonts:nnnnn { #1 } { #2 } { #3 } { #4 } { #5 }
  }
  {
    \ClassError { novathesis }
      { Font~style~" #2 "~is~not~compatible~with~pdfLaTeX.~
        Please~use~XeLaTeX~or~LuaLaTeX }
      { Please~use~XeLaTeX~or~LuaLaTeX } %
  }
}

% --------------------------------------------------------
% Generic download engine (used both by \ntcheckfonts above and by
% \ntchecksdgs further down).  Messages are intentionally generic: this
% engine has no notion of "font" or "SDG", it just fetches a URL to a
% local path.
% --------------------------------------------------------
\msg_new:nnn { ntdownload } { shell-escape-required }
{
  File~download~requires~shell~escape.~\\
  Please~add~-shell-escape~to~your~compilation~command.
}

% Portable null device: /dev/null on Unix, nul on Windows (used to silence
% "command not found" stderr from the tool-availability probes below).
\sys_if_platform_windows:TF
  { \tl_const:Nn \c__nt_devnull_tl { nul } }
  { \tl_const:Nn \c__nt_devnull_tl { /dev/null } }

% \sys_get_shell:nnN does NOT expand its command, so \c__nt_devnull_tl would
% reach the shell verbatim and create a stray file named after the macro.
% Use an e-expanding variant for the probes so the null device is resolved.
\cs_generate_variant:Nn \sys_get_shell:nnN { enN }

\msg_new:nnn { ntdownload } { download-failed }
{
  Could~not~download~'#2'~from~'#1'.~\\
  No~file~was~created;~check~your~network~connection,~or~fetch~it~
  manually~from~that~URL.
}

\seq_new:N \l__nt_variants_seq
\tl_new:N \l__nt_filename_tl
\tl_new:N \l__nt_remote_url_tl
\tl_new:N \l__nt_local_path_tl
\str_new:N \l__nt_download_check_str

% --------------------------------------------------------
% SECURITY: validate every download argument (used both for fonts and
% for SDGs) before it is interpolated into a shell command.  These
% functions only ever run with shell escape enabled, so an argument
% carrying shell metacharacters could execute arbitrary commands during
% the download.  Names, formats, variants, languages and list entries
% must be plain identifiers ([A-Za-z0-9._-]); URLs and target paths may
% not contain shell metacharacters.  Every component of the download
% URL and local path is checked here, and the shell commands below
% additionally single-quote each field.
\cs_generate_variant:Nn \regex_match:nnTF { nV }

\msg_new:nnnn { ntdownload } { unsafe-token }
  { Refusing~to~download:~unsafe~#1~'#2'. }
  {
    The~#1~may~contain~only~letters,~digits,~'.',~'_'~and~'-'.
    A~value~with~other~characters~could~inject~shell~commands
    while~downloading~the~file,~so~it~is~rejected.
  }

\msg_new:nnnn { ntdownload } { unsafe-path }
  { Refusing~to~download:~unsafe~#1~'#2'. }
  {
    The~#1~contains~a~shell~metacharacter.~Such~a~value~could
    inject~shell~commands~while~downloading~the~file,~so~it~is
    rejected.
  }

% \__nt_download_assert_token:nn {label} {value} — value must be a
% plain identifier; otherwise abort with an error naming the field.
\cs_new_protected:Npn \__nt_download_assert_token:nn #1 #2
  {
    \str_set:Ne \l__nt_download_check_str { #2 }
    \regex_match:nVF { \A[A-Za-z0-9._\-]+\Z } \l__nt_download_check_str
      { \msg_error:nnxx { ntdownload } { unsafe-token }
          { #1 } { \l__nt_download_check_str } }
  }

% \__nt_download_assert_path:nn {label} {value} — value must be free of
% shell metacharacters (quote/backtick/$/&/;/|/<>/()/backslash/newline).
\cs_new_protected:Npn \__nt_download_assert_path:nn #1 #2
  {
    \str_set:Ne \l__nt_download_check_str { #2 }
    \regex_match:nVT { [`'"$&;|<>()\\\n] } \l__nt_download_check_str
      { \msg_error:nnxx { ntdownload } { unsafe-path }
          { #1 } { \l__nt_download_check_str } }
  }

\cs_new_protected:Npn \nt_check_fonts:nnnnn #1 #2 #3 #4 #5
{
  % Validate every argument before it can reach a shell command.
  \__nt_download_assert_token:nn { format }       { #1 }
  \__nt_download_assert_token:nn { font~name }    { #2 }
  \__nt_download_assert_path:nn  { base~URL }     { #4 }
  \__nt_download_assert_path:nn  { target~folder }{ #5 }

  % Split variants by commas, then validate each one
  \seq_set_split:Nnn \l__nt_variants_seq { , } { #3 }
  \seq_map_inline:Nn \l__nt_variants_seq
    { \__nt_download_assert_token:nn { font~variant } { ##1 } }

  % Create target directory if it doesn't exist.  (\sys_shell:n is not a real
  % l3 function; use \sys_shell_now:x, which is defined and expands #5.)
  \file_if_exist:nF { #5/DO_NOT_REMOVE_THIS_FILE.txt }
  {
    \sys_if_platform_windows:TF
      { \sys_shell_now:x { mkdir~"#5" } }      % cmd.exe: creates intermediate dirs
      { \sys_shell_now:x { mkdir~-p~'#5' } }
  }

  % Process each variant
  \seq_map_inline:Nn \l__nt_variants_seq
  {
    \tl_set:Nn \l__nt_filename_tl { #2-##1.#1 }
    \tl_set:Nn \l__nt_remote_url_tl { #4/#2/#2-##1.#1 }
    \tl_set:Nn \l__nt_local_path_tl { #5/#2-##1.#1 }

    % Check if file already exists
    \file_if_exist:nTF { \l__nt_local_path_tl }
    {
      \iow_term:x { Font~' \l__nt_filename_tl '~already~exists. }
    }
    {
      \iow_term:x { Downloading~font~' \l__nt_filename_tl '~
                    from~' \l__nt_remote_url_tl '~
                    to~' \l__nt_local_path_tl '~ }

      % Download the font
      \nt_download_file:nn { \l__nt_remote_url_tl } { \l__nt_local_path_tl }
    }
  }
}


\tl_new:N \l_nt_cmd_output_tl

% "A download tool has already handled this URL" flag.  Uses its own name
% rather than the kernel's shared \l_tmpa_bool because it must survive across
% the \sys_get_shell:enN / \sys_shell_now:x probe calls below.
\bool_new:N \l__nt_download_done_bool

% When true, \nt_download_file:nn does not warn if no file was produced: the
% caller takes care of reporting (used by the SDG code, where a miss usually
% just means "this language has no icons upstream").
\bool_new:N \l__nt_download_quiet_bool

\str_new:N \l__nt_download_size_str

% \__nt_download_discard:n {path} — remove a file left behind by a failed
% transfer.  Needed because "wget -O" creates the output file *before* the
% request is answered, so an HTTP error or a dead network leaves a 0-byte
% file; without this it would be cached forever and later break
% \includegraphics instead of being reported as a failed download.
\cs_new_protected:Npn \__nt_download_discard:n #1
  {
    \iow_term:x { Discarding~empty~file~'#1'. }
    \sys_if_platform_windows:TF
      { \sys_shell_now:x { del~/q~"#1" } }
      { \sys_shell_now:x { rm~-f~'#1' } }
  }

% \__nt_download_accept:n {path} — decide whether the tool that has just run
% actually delivered the file.  Only a non-empty file counts as success; on
% anything else the download is *not* marked done, so the next tool in the
% chain is tried (a present but failing curl must not block wget/python).
\cs_new_protected:Npn \__nt_download_accept:n #1
  {
    \file_get_size:nNT { #1 } \l__nt_download_size_str
      {
        \str_if_eq:VnTF \l__nt_download_size_str { 0 }
          { \__nt_download_discard:n { #1 } }
          { \bool_set_true:N \l__nt_download_done_bool }
      }
  }

\cs_new_protected:Npn \nt_download_file:nn #1 #2
{
  % #1 = remote URL, #2 = local target path
  % \l__nt_download_done_bool means "stop trying": it is set only once a tool
  % has produced a usable (non-empty) file, so the strategies below form a real
  % fallback chain instead of "whichever tool is installed first wins".
  \bool_set_false:N \l__nt_download_done_bool

  % A download tool can only run with (unrestricted) shell escape.
  \sys_if_shell_unrestricted:F
    {
      \msg_error:nn { ntdownload } { shell-escape-required }
      \bool_set_true:N \l__nt_download_done_bool
    }

  % Probe curl (redirect only stderr so the version string is captured).
  \bool_if:NF \l__nt_download_done_bool
    {
      \iow_term:x { Trying:~'curl'... }
      \sys_get_shell:enN  {curl ~ --version ~ 2> ~ \c__nt_devnull_tl }
                          { \tl_trim_spaces:n }
                          \l_nt_cmd_output_tl
      \tl_if_empty:NF \l_nt_cmd_output_tl
        {
          % -f: fail (no file written) on HTTP >=400, so the file-existence
          % check below is meaningful; -sS: quiet but still report real errors.
          \iow_term:x { Running: ~ curl ~ -fsSL ~ -o ~ '#2' ~ '#1'... }
          \sys_shell_now:x { curl ~ -fsSL ~ -o ~ '#2' ~ '#1' }
          \__nt_download_accept:n { #2 }
        }
    }

  % Probe wget.  Unlike curl, wget has no "-f": with -O it always creates the
  % output file, and leaves it empty on an HTTP error or a dead network, so the
  % 0-byte result is caught (and removed) by \__nt_download_accept:n below.
  \bool_if:NF \l__nt_download_done_bool
    {
      \iow_term:x { Trying: ~ 'wget'... }
      \sys_get_shell:enN  {wget ~ --version ~ 2> ~ \c__nt_devnull_tl }
                          { \tl_trim_spaces:n }
                          \l_nt_cmd_output_tl
      \tl_if_empty:NF \l_nt_cmd_output_tl
        {
          \iow_term:x { Running: ~ wget ~ -q ~ --tries=1 ~ -O ~ '#2' ~ '#1'... }
          \sys_shell_now:x { wget ~ -q ~ --tries=1 ~ -O ~ '#2' ~ '#1' }
          \__nt_download_accept:n { #2 }
        }
    }

  % Probe python3 (each interpreter runs only itself — no cross-calls).
  % The probe imports urllib.request instead of asking for --version: that is
  % what the download actually needs, and it is Python 3 only (see 'python'
  % below).  It must print something, as a blank result means "not usable".
  \bool_if:NF \l__nt_download_done_bool
    {
      \iow_term:x { Trying: ~ 'python3'... }
      % NOTE: single quotes.  \sys_get_shell:nnN refuses a command containing a
      % double quote and returns \q_no_value, which is NOT empty and would make
      % the probe succeed for a tool that is not there.
      \sys_get_shell:enN  {python3 ~ -c ~ 'import ~ urllib.request; ~
                           print(1)' ~ 2> ~ \c__nt_devnull_tl }
                          { \tl_trim_spaces:n }
                          \l_nt_cmd_output_tl
      \tl_if_empty:NF \l_nt_cmd_output_tl
        {
          \iow_term:x { Running: ~ python3 ~ urlretrieve('#1',~'#2')... }
          \sys_shell_now:x
            {
              python3 ~ -c ~ "import ~ urllib.request; ~
              urllib.request.urlretrieve('#1', ~ '#2')"
            }
          \__nt_download_accept:n { #2 }
        }
    }

  % Probe python (reached only when python3 was not found).  On a system where
  % 'python' is still Python 2 the import below fails, the probe prints
  % nothing, and this branch is skipped: urllib.request does not exist there
  % (it is urllib.urlretrieve), so '--version' would have wrongly accepted it.
  \bool_if:NF \l__nt_download_done_bool
    {
      \iow_term:x { Trying: ~ 'python'... }
      \sys_get_shell:enN  {python ~ -c ~ 'import ~ urllib.request; ~
                           print(1)' ~ 2> ~ \c__nt_devnull_tl }
                          { \tl_trim_spaces:n }
                          \l_nt_cmd_output_tl
      \tl_if_empty:NF \l_nt_cmd_output_tl
        {
          \iow_term:x { Running: ~ python ~ urlretrieve('#1',~'#2')... }
          \sys_shell_now:x
            {
              python ~ -c ~ "import ~ urllib.request; ~
              urllib.request.urlretrieve('#1', ~ '#2')"
            }
          \__nt_download_accept:n { #2 }
        }
    }

  % Windows fallback: PowerShell (probe with 'where', not a malformed 'powershell /?"').
  \bool_if:NF \l__nt_download_done_bool
    {
      \sys_if_platform_windows:T
        {
          \iow_term:x { Trying: ~ 'powershell'... }
          \sys_get_shell:enN  {where ~ powershell ~ 2> ~ \c__nt_devnull_tl }
                              { \tl_trim_spaces:n }
                              \l_nt_cmd_output_tl
          \tl_if_empty:NF \l_nt_cmd_output_tl
            {
              \iow_term:x
                {
                  powershell ~ -Command ~
                  "Invoke-WebRequest ~ -Uri ~ '#1' ~ -OutFile ~ '#2'"
                }
              \sys_shell_now:x
                {
                  powershell ~ -Command ~
                  "Invoke-WebRequest ~ -Uri ~ '#1' ~ -OutFile ~ '#2'"
                }
              \__nt_download_accept:n { #2 }
            }
        }
    }

  % Report when no tool managed to deliver the file (only when shell escape was
  % available; the disabled case already raised its own error above).  This
  % turns a silent failure (404, network down, no tool found) into an
  % actionable warning instead of a later "file not found" error.  The test is
  % on the flag, not on the file, because an empty file left by a failed
  % transfer has just been removed by \__nt_download_accept:n.
  \sys_if_shell_unrestricted:T
    {
      \bool_if:NF \l__nt_download_done_bool
        {
          \bool_if:NF \l__nt_download_quiet_bool
            { \msg_warning:nnnn { ntdownload } { download-failed } { #1 } { #2 } }
        }
    }
}


% --------------------------------------------------------
% Automatically download necessary SDG icons.  Unlike fonts, SDG icons
% are plain PDF images, so this works under any engine (pdfLaTeX,
% XeLaTeX or LuaLaTeX), not just XeLaTeX/LuaLaTeX.  Called from
% \ntprintsdgs (nt-render-matter.sty) once per printed abstract, with
% that abstract's own language and the active icon type (see
% print/sdgs/list and print/sdgs/type below).
%
% Source: https://github.com/joaomlourenco/SDGs.  The upstream folder for
% a language is simply its novathesis language code in UPPER CASE (en ->
% EN, pt -> PT, ...), so adding a language upstream needs NO change here,
% as long as the new folder is named after the matching language code.
% Inside "<LANG>/PDF/" the repo nests one folder per colour variant
% (Normal, Inverted, BlackWhite — see \__nt_set_sdg_variant:n below); a
% language that does not (yet) offer a given variant costs exactly one
% failed request per run (the loop stops at the first miss) and then
% falls back to English, matching \ntprintsdgs's own "fall back to
% sdg-en" behaviour below.
% --------------------------------------------------------

\tl_new:N \l__nt_sdg_lang_tl
\tl_new:N \l__nt_sdg_type_tl
\tl_new:N \l__nt_sdg_remote_lang_tl
\tl_new:N \l__nt_sdg_variant_tl
\tl_new:N \l__nt_sdg_local_path_tl
\bool_new:N \l__nt_sdg_lang_ok_bool

\tl_const:Nn \c__nt_sdg_base_url_tl
  { https://raw.githubusercontent.com/joaomlourenco/SDGs/main/Icons }

% The upstream repo keeps one sub-folder per colour variant (PDF/Normal,
% PDF/Inverted, PDF/BlackWhite) inside each language folder; the file
% itself still carries the -n/-i/-m suffix.  Map the internal type letter
% (see print/sdgs/type above) to that folder name.  #2 is already
% restricted to i/n/m by the option's own choice list, so an unmatched
% case here can only mean the upstream layout changed again.
\msg_new:nnn { ntdownload } { sdg-type-unknown }
  { Unknown~SDG~icon~type~'#1'.~Expected~one~of~i,~n,~m. }
\cs_new_protected:Npn \__nt_set_sdg_variant:n #1
  {
    \str_case:nnF { #1 }
      {
        { i } { \tl_set:Nn \l__nt_sdg_variant_tl { Inverted } }
        { n } { \tl_set:Nn \l__nt_sdg_variant_tl { Normal } }
        { m } { \tl_set:Nn \l__nt_sdg_variant_tl { BlackWhite } }
      }
      { \msg_error:nnx { ntdownload } { sdg-type-unknown } { #1 } }
  }

\msg_new:nnn { ntdownload } { sdg-lang-unavailable }
{
  Could~not~download~the~SDG~icons~for~language~'#1'~from~'#2'.~\\
  This~normally~means~that~this~language~is~not~(yet)~available~in~
  https://github.com/joaomlourenco/SDGs,~in~which~case~the~English~icons~
  are~used~instead~and~you~can~ignore~this~warning.~If~the~language~IS~
  available~there,~check~your~network~connection.
}

% \__nt_check_sdgs_lang:nn {novathesis-lang} {sdg-type} — ensures every
% SDG icon in print/sdgs/list, of the given type (i/n/m), is available
% locally in novathesisFiles/Images/sdg-<lang>, downloading whatever is
% missing.  Stops at the first download that produces no file, so a
% language that is not available upstream costs one request, not one per
% icon; the caller then falls back to English.
\cs_new_protected:Npn \__nt_check_sdgs_lang:nn #1 #2
  {
    \__nt_download_assert_token:nn { SDG~language } { #1 }
    \__nt_download_assert_token:nn { SDG~type } { #2 }
    % Upstream folder = language code in upper case; no table to maintain.
    \tl_set:Nx \l__nt_sdg_remote_lang_tl { \str_uppercase:n { #1 } }
    % Upstream now nests a colour-variant folder (Normal/Inverted/BlackWhite)
    % under PDF/ for every language; compute it once per call.
    \__nt_set_sdg_variant:n { #2 }
    \bool_set_true:N \l__nt_sdg_lang_ok_bool
    \file_if_exist:nF { \DIRIMAGES/sdg-#1/DO_NOT_REMOVE_THIS_FILE.txt }
      {
        \sys_if_platform_windows:TF
          { \sys_shell_now:x { mkdir~"\DIRIMAGES/sdg-#1" } }
          { \sys_shell_now:x { mkdir~-p~'\DIRIMAGES/sdg-#1' } }
      }
    \optionlistdo{/novathesis/print/sdgs/list}
      {
        \bool_if:NT \l__nt_sdg_lang_ok_bool
          {
            \__nt_download_assert_token:nn { SDG~number } { ##1 }
            \tl_set:Nn \l__nt_filename_tl { sdg-#1/##1-#2.pdf }
            \tl_set:Nx \l__nt_remote_url_tl
              { \c__nt_sdg_base_url_tl/\l__nt_sdg_remote_lang_tl/PDF/%
                \l__nt_sdg_variant_tl/##1-#2.pdf }
            \tl_set:Nn \l__nt_sdg_local_path_tl { \DIRIMAGES/sdg-#1/##1-#2.pdf }

            \file_if_exist:nTF { \l__nt_sdg_local_path_tl }
              {
                \iow_term:x { SDG~icon~' \l__nt_filename_tl '~already~exists. }
              }
              {
                \iow_term:x { Downloading~SDG~icon~' \l__nt_filename_tl '~
                              from~' \l__nt_remote_url_tl '~
                              to~' \l__nt_sdg_local_path_tl '~ }
                % Quiet: a miss here usually just means "no icons upstream for
                % this language", which is reported once below, not per icon.
                \bool_set_true:N \l__nt_download_quiet_bool
                \nt_download_file:nn
                  { \l__nt_remote_url_tl }
                  { \l__nt_sdg_local_path_tl }
                \bool_set_false:N \l__nt_download_quiet_bool
                \file_if_exist:nF { \l__nt_sdg_local_path_tl }
                  {
                    \bool_set_false:N \l__nt_sdg_lang_ok_bool
                    \msg_warning:nnxx { ntdownload } { sdg-lang-unavailable }
                      { #1 } { \l__nt_remote_url_tl }
                  }
              }
          }
      }
  }

% \nt_check_sdgs:nn {novathesis-lang} {sdg-type} — checks/downloads the
% abstract's own language (if available upstream) and always also the
% English set, as the safety net \ntprintsdgs itself falls back to.
\cs_new_protected:Npn \nt_check_sdgs:nn #1 #2
  {
    \tl_set:Ne \l__nt_sdg_lang_tl { #1 }
    \tl_set:Ne \l__nt_sdg_type_tl { #2 }
    \exp_args:NVV \__nt_check_sdgs_lang:nn \l__nt_sdg_lang_tl \l__nt_sdg_type_tl
    \tl_if_eq:nnF { \l__nt_sdg_lang_tl } { en }
      { \exp_args:NnV \__nt_check_sdgs_lang:nn { en } \l__nt_sdg_type_tl }
  }

% Public entry point, mirroring \ntcheckfonts.
\NewDocumentCommand { \ntchecksdgs } { m m }
  { \nt_check_sdgs:nn { #1 } { #2 } }


% --------------------------------------------------------
% Trim spaces around argument
\cs_new_eq:NN \trimspaces \tl_trim_spaces:e





%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% INTERNAL OPTIONS & LANGUAGE MAPS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% The list of languages actually found on disk (nt-setup.sty populates
% this once babel/strings-*.ldf loading resolves which languages exist).
% Was an options-package list; \@nt@langloaded@reset/@push are
% plain-named so the classical, catcode-sensitive \InputIfFileExists
% loop in nt-setup.sty can call them without needing ExplSyntaxOn active
% at the call site (see the warning on that loop for why).
\seq_new:N \g__nt_lang_loaded_seq
\cs_new_protected:Npn \@nt@langloaded@reset
  { \seq_gclear:N \g__nt_lang_loaded_seq }
\cs_new_protected:Npn \@nt@langloaded@push #1
  { \seq_gput_right:Ne \g__nt_lang_loaded_seq {#1} }
\ExplSyntaxOff


% LangLong: maps 2/3-letter language codes to babel language names
\NewMemStore{LangLong}
\SetLangLong(cat):={catalan}
\SetLangLong(cz):={czech}
\SetLangLong(de):={german}
\SetLangLong(dk):={danish}
\SetLangLong(en):={english}
\SetLangLong(es):={spanish}
\SetLangLong(fr):={french}
\SetLangLong(gr):={greek}
\SetLangLong(it):={italian}
\SetLangLong(nl):={dutch}
\SetLangLong(pl):={polish}
\SetLangLong(pt):={portuguese}
\SetLangLong(sk):={slovak}
\SetLangLong(uk):={ukrainian}
\SetLangLong(zhs):={zh-Hans}
\SetLangLong(zht):={zh-Hant}


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% MANAGE FOLDERS PER CLASS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\ExplSyntaxOn
\prop_const_from_keyval:Nn \c__nt_folders_prop
  {
    copyright = { . },
    frontpage = { \DIRFRONTMATTER },
    statement = { \DIRFRONTMATTER },
    cover = { \DIRFRONTMATTER },
    dedication = { \DIRFRONTMATTER },
    acknowledgements = { \DIRFRONTMATTER },
    quote = { \DIRFRONTMATTER },
    abstract = { \DIRFRONTMATTER },
    glossaries = { \DIRFRONTMATTER },
    chapter = { \DIRMAINMATTER },
    appendix = { \DIRBACKMATTER },
    annex = { \DIRBACKMATTER },
    bib = { \DIRBIBLIOGRAPHY },
  }



%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% MANAGE DOCUMENT CLASSES
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\prop_const_from_keyval:Nn \c__nt_docclass_prop
  {
    phd = phd, phdprop = phd, phdplan = phd,
    msc = msc, mscplan = msc,
    bsc = bsc,
    plain = plain,
  }

% --------------------------------------------------------
% Check if the document is of a specific class, i.e., bsc, msc, phd or main
% etoolbox's \ifstrequal does not reliably compare \@DOCTYPE/\@DOCCLASS
% (looked up via l3prop) against a literal string -- str_if_eq:ee (fully
% catcode-blind expl3 string comparison) is used for both instead.
% "Main" doctypes are exactly the ones docclass maps to themselves
% (phd->phd, but phdprop->phd and phdplan->phd).
\cs_new_protected:Npn \ifdocclass #1
  { \str_if_eq:eeTF {\@DOCCLASS} {#1} }
\cs_new_protected:Npn \ifmaindoc #1#2
  { \str_if_eq:eeTF {\@DOCTYPE} {\@DOCCLASS} {#1} {#2} }

\ExplSyntaxOff


\newcommand{\ifphddoc}{\ifdocclass{phd}}
\newcommand{\ifmscdoc}{\ifdocclass{msc}}
\newcommand{\ifbscdoc}{\ifdocclass{bsc}}
\newcommand{\ifplaindoc}{\ifdocclass{plain}}

\newcommand{\ifdocstatus}[1]{\ifoptionequal{/novathesis/docstatus}{#1}}
\newcommand{\ifdocfinal}{\ifdocstatus{final}}
\newcommand{\ifdocprovisional}{\ifdocstatus{provisional}}
\newcommand{\ifdocworking}{\ifdocstatus{working}}
\newcommand{\ifdocunset}{\ifdocstatus{unset}}


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% MANAGE PERSON LISTS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Storage: one seq per field (name/title/position/institution/gender),
% per (class,role) -- e.g. g__nt_person_adviser_a_name_seq -- with index n
% describing the same person across all five seqs. \ntaddperson pushes into
% all five together; \ntforeachperson/\ntpersoncount/\ntpersongender read
% them back. Kept in a single \ExplSyntaxOn block below so every "_" in a
% \csname is tokenized as a letter at definition time and stays that way
% however it's later called (see [[established pattern from Tier 1-4a]]).
\ExplSyntaxOn
\keys_define:nn { nt/person }
  {
    title       .tl_set:N  = \l__nt_person_title_tl,
    title       .initial:n = {},
    position    .tl_set:N  = \l__nt_person_position_tl,
    position    .initial:n = {},
    institution .tl_set:N  = \l__nt_person_institution_tl,
    institution .initial:n = {},
  }
\cs_new:Npn \@nt@trim@aux #1 { \tl_trim_spaces:e {#1} }
\cs_new_protected:Npn \@nt@person@setkeys #1
  { \keys_set:nn { nt/person } { title={}, position={}, institution={}, #1 } }
\cs_new_protected:Npn \@nt@person@store #1#2#3#4#5#6#7
  {
    % #1=class #2=role #3=name #4=title #5=position #6=institution #7=gender
    \seq_if_exist:cF { g__nt_person_#1_#2_name_seq }
      {
        \seq_new:c { g__nt_person_#1_#2_name_seq }
        \seq_new:c { g__nt_person_#1_#2_title_seq }
        \seq_new:c { g__nt_person_#1_#2_position_seq }
        \seq_new:c { g__nt_person_#1_#2_institution_seq }
        \seq_new:c { g__nt_person_#1_#2_gender_seq }
      }
    \seq_gput_right:ce { g__nt_person_#1_#2_name_seq }        {#3}
    \seq_gput_right:ce { g__nt_person_#1_#2_title_seq }       {#4}
    \seq_gput_right:ce { g__nt_person_#1_#2_position_seq }    {#5}
    \seq_gput_right:ce { g__nt_person_#1_#2_institution_seq } {#6}
    \seq_gput_right:ce { g__nt_person_#1_#2_gender_seq }      {#7}
  }

% \ntpersoncount{class}{role} -> number of people registered (0 if none).
% Fully expandable (no TF-conditional composition tricks -- those proved
% unreliable inside \cs_new:Npn; \seq_if_exist:cTF alone is fine here).
\cs_new:Npn \ntpersoncount #1#2
  { \seq_if_exist:cTF { g__nt_person_#1_#2_name_seq }
      { \seq_count:c { g__nt_person_#1_#2_name_seq } }
      { 0 }
  }

% \ntpersongender{class}{role}{result-macro} -> assigns "m" into
% <result-macro> if any registered person is male, else "f" (matches the
% previous \@nt@reducegender rule). A procedure, not an expandable
% function: \seq_if_in:...TF did not compose reliably as a return value,
% so this walks the seq with \seq_map_inline:Nn + \str_if_eq:nnT instead,
% both proven throughout this migration.
\cs_new_protected:Npn \ntpersongender #1#2#3
  {
    \tl_set:Nn #3 {f}
    \seq_if_exist:cT { g__nt_person_#1_#2_gender_seq }
      { \expandafter\seq_map_inline:Nn\csname g__nt_person_#1_#2_gender_seq\endcsname
          { \str_if_eq:nnT {##1} {m} { \tl_set:Nn #3 {m} } }
      }
  }

% \ntforeachperson{class}{role}{<macro>} -> calls
% <macro>{title}{name}{position}{institution}{gender} once per person
% registered in that (class,role), in \ntaddperson call order. No-op if
% nobody is registered.
\NewDocumentCommand \ntforeachperson { m m m }
  { \seq_if_exist:cT { g__nt_person_#1_#2_name_seq }
      { \int_step_inline:nn { \seq_count:c { g__nt_person_#1_#2_name_seq } }
          { #3
              { \seq_item:cn { g__nt_person_#1_#2_title_seq }       {##1} }
              { \seq_item:cn { g__nt_person_#1_#2_name_seq }        {##1} }
              { \seq_item:cn { g__nt_person_#1_#2_position_seq }    {##1} }
              { \seq_item:cn { g__nt_person_#1_#2_institution_seq } {##1} }
              { \seq_item:cn { g__nt_person_#1_#2_gender_seq }      {##1} }
          }
      }
  }

% \@nt@addperson@i -- \ntaddperson's real implementation, split out so the
% trailing [title=...,position=...,institution=...] can be an xparse O{}
% slot (xparse needs to own the argument that follows to peek for "[").
% #1=class #2=role #3=gender #4=name-or-legacy-string #5=[keys]
\NewDocumentCommand \@nt@addperson@i { m m m m O{} } {%
  \IfSubStr{#4}{,}{%
    % Legacy "name, position, institution" comma string (still
    % supported): cut only the first two commas -- any further commas
    % belong to the institution (e.g. "..., NOVA School of Science and
    % Technology, UNL").
    \ClassWarning{novathesis}{%
      \string\ntaddperson: the "name,\space position,\space institution"
      comma string is deprecated.\MessageBreak
      Use \string\ntaddperson\string{#1\string}(#2,#3)\string{name\string}%
      [position=...,\space institution=...]\space instead%
    }%
    \noexpandarg
    \StrCut{#4}{,}\@nt@p@name\@nt@p@rest
    \fullexpandarg
    \StrCut{\@nt@p@rest}{,}\@nt@p@position@raw\@nt@p@institution@raw
    \edef\@nt@p@position{\@nt@trim@aux{\@nt@p@position@raw}}%
    \edef\@nt@p@institution{\@nt@trim@aux{\@nt@p@institution@raw}}%
    \def\@nt@p@title{}%
  }{%
    \def\@nt@p@name{#4}%
    \@nt@person@setkeys{#5}%
    \let\@nt@p@title\l__nt_person_title_tl
    \let\@nt@p@position\l__nt_person_position_tl
    \let\@nt@p@institution\l__nt_person_institution_tl
  }%
  \@nt@person@store{#1}{#2}{\@nt@p@name}{\@nt@p@title}{\@nt@p@position}{\@nt@p@institution}{#3}%
}
\ExplSyntaxOff

\def\ntaddperson#1(#2,#3)#4{%
  % syntax: \ntaddperson{class}(role,gender){name}[title=...,position=...,institution=...]
  %         legacy:  \ntaddperson{class}(role,gender){name, position, institution}
  % #1=adviser | committee
  % #2={a,c}            {c,r,a,m,g}
  %     a=adviser        c=chair
  %     c=coadviser      r=rapporteur/referee
  %                      a=adviser
  %                      ca=adviser
  %                      m=other members
  %                      g=guests
  % #3={m,f}
  % #4=name (or, legacy, "name, position, institution")
  \@nt@addperson@i{#1}{#2}{#3}{#4}%
}


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% MANAGE FILE LISTS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand*{\ntaddfile}[1]{%
  % syntax: \ntaddfile{class}[key]{filename}
  %         #1=file family (quote, abstract, chapter, etc)
  %         #2=[key] (OPTIONAL)
  %         #3=filename
  \@nt@addtolist{file}{#1}%
}

\ExplSyntaxOn
% Registry of which file/person classes have ever been registered at
% all (a "registry of registries"): was an options-package list, now a
% plain dedup-guarded seq -- \seq_if_in:Nn compares its item argument
% unexpanded, which is safe here since every caller (\ntaddfile,
% \ntaddperson) passes a literal identifier, never a macro.
\seq_new:N \g__nt_listtypes_seq
\clist_new:N \l__nt_addtolist_clist
\NewDocumentCommand {\@nt@addtolist}
                    {m m O{\option{/novathesis/doctype}} m} {%
  % syntax: \ntaddto{type}{class}[key]{filename}
  %         #1 ={file, person}
  %         #2=family {quote, abstract, chapter, etc} or {author, adviser, committee}
  %         #3=[file-lang | sub-family | key] (OPTIONAL)
  %         #4=filename
  \seq_if_in:NnF \g__nt_listtypes_seq {#2}
      { \seq_gput_right:Nn \g__nt_listtypes_seq {#2} }
  \IfBeginsWithAnyOf{#4}{/,.}
      {\xdef\@nt@filewithpath{#4}}
      {\xdef\@nt@filewithpath{\prop_item:Ne \c__nt_folders_prop {#2}/#4}}
  % #3 is fully expanded before being split into items, matching the
  % previous \@for\nt@tmpi:=\expanded{#3}\do{...} behaviour; the items
  % are file-lang/sub-family/key identifiers, not document text.
  \clist_set:Ne \l__nt_addtolist_clist {#3}
  % Storage key mirrors the old options-package path "/@nt/#1/#2/##1"
  % literally (slashes and all) as the \csname suffix, so every reader
  % of that same path string -- \iffileadded, \@nt@loadallfiles,
  % \@nt@foralllist -- keeps matching it exactly, without having to
  % agree on a separate segmentation scheme.
  \clist_map_inline:Nn \l__nt_addtolist_clist
      {
        \seq_if_exist:cF { g__nt_filereg/@nt/#1/#2/##1 }
            { \seq_new:c { g__nt_filereg/@nt/#1/#2/##1 } }
        \seq_gput_right:ce { g__nt_filereg/@nt/#1/#2/##1 } {\@nt@filewithpath}
      }
}
\ExplSyntaxOff

\ExplSyntaxOn
\NewDocumentCommand {\@nt@foralllist} { m m m } {%
  % #1={file, person}
  % #2=file family (quote, abstract, chapter, etc) -- may itself embed
  %    further "/"-separated sub-path segments (e.g. "glossaries/\nt@glstype")
  % #3=macro to be applied, which will receive the item from the list
  \seq_if_exist:cT { g__nt_filereg/@nt/#1/#2 }
      { \expandafter\seq_map_inline:Nn\csname g__nt_filereg/@nt/#1/#2\endcsname {#3{##1}} }%
}
\ExplSyntaxOff


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Multilingual support
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\newcommand*{\ntselectlang}[1]{%
  \xdef\@LSHORT{#1}%
  \xdef\@LLONG{\GetLangLong(#1)}%
  \selectlanguage{\@LLONG}%
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% DATA INITIALIZATION
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\newcommand*{\initmemory}[2][\GetNotDefinedString(\@LANG@COVER)]{%
  \optionlistdo{/novathesis/lang/all}{\MemSet*{#2}{##1}{#1}}}


% For PDF bookmarks
\NewMemStore{KeywordsBlockPre}
\NewMemStore{KeywordsBlockPost}
\NewMemStore{KeywordsSep}
\NewMemStore{KeywordsStringPrefix}
\NewMemStore{KeywordsStringSuffix}
\NewMemStore{KeywordsStringFont}
\SetKeywordsBlockPre()={\bigskip\noindent}
\SetKeywordsStringSuffix()={:\enspace}
\SetKeywordsStringFont()={\bfseries}
\SetKeywordsSep()={~{\textperiodcentered}~}

% Stores the raw keywords per language (for PDF metadata)
\NewMemStore{DocKeywords}

\NewMemStore{Sponsor}

\NewMemStore{CommaString}
\optionlistdo{/novathesis/lang/all}{%
  \SetCommaString*(#1)={, }%
}



\NewMemStore{DegreeStringFont}

\NewMemStore{DegreeName}
\NewMemStore{DegreeNameFont}

\NewMemStore{DegreeNamePrefixFont}

\NewMemStore{DoctypeStringFont}


\MemSet{adviserremainderfont}{}{\itshape\scriptsize}

% Committee stuff
\NewMemStore{AdviserOrder}
\SetAdviserOrder()={a,c}  % By default print advisers and co-advisers

% AbstractOrder: for each main language, list which abstracts to print and in which order
\NewMemStore{AbstractOrder}

\NewMemStore{CommitteeOrder}         %e.g., "{c,r,a,m,g}"

\MemSet{committeeremainderfont}{}{\itshape\scriptsize}

\NewMemStore{DissertationStringFont}
\SetDissertationStringFont()={}% default: no font override


\NewMemStore{MajorField}
\NewMemStore{MinorField}
\NewMemStore{Specialization}

% Author's date/place of birth: required on the other/huberlin cover only,
% but declared here (rather than in that school's .clo) so that
% \SetAuthorBirthDate*/\SetAuthorBirthPlace* are always valid commands in
% 0-Config/3_cover.tex, regardless of which school is selected -- otherwise
% switching away from other/huberlin without also removing those lines from
% 3_cover.tex would fail with "Undefined control sequence". Same reasoning
% as MajorField/Specialization above: declared globally, only some schools
% render it.
\NewMemStore{AuthorBirthDate}
\NewMemStore{AuthorBirthPlace}

\NewMemStore{Spine}
\NewMemStore{ThesisCover}
\NewMemStore{Margin}
\NewMemStore{FrontPageImage}

\NewMemStore{University}
\NewMemStore{School}

\NewMemStore{DocTitle}


% For each main language, list which abstracts shall be printed and in which order
% may have more than two (just be sure you include the languages above as well)
% (migrated to memstore — \NewMemStore{AbstractOrder} is declared above)



% --------------------------------------------------------
% PROCESSING CLASS OPTIONS
% Bridges \documentclass[key=value,...]{novathesis} into the option tree
% -- documented syntax (see app-internals.tex), though no real school
% config actually uses it (\documentclass{novathesis} is always called
% bare everywhere in this repo).
%
% \@classoptionslist is the WRONG kernel variable to read here: it is
% only ever populated by \ProcessOptions actually running, which
% novathesis.cls never calls (confirmed by grep) -- reading it raised
% "the key '\@classoptionslist' is unknown" instead of a comma-list. The
% old options.sty-based \options@ProcessOptions{/novathesis} avoided this
% because \option@ProcessClassOptions (see options.sty) reads a
% DIFFERENT, more fundamental kernel variable instead: \opt@<name>.<ext>
% (here, \opt@novathesis.cls), which \documentclass/\LoadClass populate
% with the class's raw bracketed options REGARDLESS of whether
% \ProcessOptions ever runs. Reusing that same variable here reproduces
% the old behaviour correctly instead of dropping the feature.
\ifcsvoid{opt@\@currname.\@currext}{}{%
  \ExplSyntaxOn
  \exp_args:Nne \keys_set:nn { novathesis } { \csname opt@\@currname.\@currext\endcsname }
  \ExplSyntaxOff
}%
% --------------------------------------------------------
% \ntsetup itself is defined in nt-options.sty (loaded from
% nt-packages.sty, before this file); the old hierarchical
% ".search also" school/university fallback it used to set up here is
% not replicated -- see that file's header for why.


\newcommand{\SpineSetup}[2][]{%
  \NTAddToHook{print/spine/pre}{%
    #1%
    \NTSetup[spine] {#2}%
  }%
}


