% \iffalse
% Copyright (C) 2026 Jonathan LAFARGE
% Development of this package was assisted by OpenAI ChatGPT; the model used
% for the current development cycle was GPT-5.6 Sol (August 2026).
%
% This work may be distributed and/or modified under the conditions of the
% LaTeX Project Public License, either version 1.3c of this license or (at your
% option) any later version. The latest version is available from
% https://www.latex-project.org/lppl/.
%
% This work has the LPPL maintenance status `author-maintained'.
%
% This work consists of the files integramme.dtx and integramme.ins and the
% derived file integramme.sty.
%
%<*driver>
\documentclass{l3doc}
\usepackage{integramme}
\usepackage{booktabs}
\usepackage{tabularx}
\usepackage{enumitem}
\OnlyDescription
\begin{document}
  \DocInput{integramme.dtx}
\end{document}
%</driver>
% \fi
%
% \GetFileInfo{integramme.sty}
%
% \title{The \textsf{integramme} package\\
%   \large Typesetting logic-grid puzzles in \LaTeX}
% \author{Jonathan LAFARGE}
% \date{Version \fileversion\ -- \filedate}
%
% \maketitle
%
% \begin{abstract}
% \textsf{integramme} typesets staircase-style logic-grid puzzles, including
% Einstein-style puzzles.  The package separates the logical data from the
% drawing: authors declare categories, items, clues and, optionally, a solution;
% the grid is generated automatically.  Printed labels may contain ordinary
% \LaTeX{} material while stable internal identifiers are used for references.
% \end{abstract}
%
% \section{Status of version 0.9.0}
%
% Version 0.9.0 is the first release candidate.  The public long-form API and
% the ordinary rendering defaults are frozen for the planned 1.0 release except
% for serious defects.  Compatibility syntax from earlier prototypes is
% retained but should not be used in new documents.
%
% Jonathan LAFARGE is the author, copyright holder and CTAN maintainer.  The
% work keeps LPPL status \texttt{author-maintained}.  Public repository, bug
% tracker and support URLs are optional CTAN communication metadata and are not
% invented for this release candidate; they may be added later.
%
% \section{Installation}
%
% During development, place \texttt{integramme.sty} where \LaTeX{} can find it.
% To generate the style file from the documented source, run:
%
% \begin{verbatim}
% latex integramme.ins
% \end{verbatim}
%
% For a local installation, move the resulting \texttt{integramme.sty} to an
% appropriate directory in your local TEXMF tree and refresh the filename
% database if your \TeX{} distribution requires it.
%
% The runtime dependency is only TikZ.  The package requires a LaTeX2e format
% dated 2022-06-01 or later.
%
% \section{Quick start}
%
% A minimal blank puzzle is declared by listing categories.  Every category
% currently has to contain the same number of items.
%
% \begin{verbatim}
% \begin{integramme}
%   \IntegrammeCategory{person}{Person}{
%     alice={Alice},
%     bruno={Bruno},
%     clara={Clara}
%   }
%   \IntegrammeCategory{drink}{Drink}{
%     tea={Tea},
%     coffee={Coffee},
%     juice={Juice}
%   }
%   \IntegrammeCategory{pet}{Pet}{
%     cat={Cat},
%     dog={Dog},
%     rabbit={Rabbit}
%   }
% \end{integramme}
% \end{verbatim}
%
% The result is generated from the data; no TikZ coordinates are needed.
%
% \begin{center}
% \begin{integramme}[cell-size=5.5mm,side-label-min-width=18mm]
%   \IntegrammeCategory{person}{Person}{
%     alice={Alice},bruno={Bruno},clara={Clara}
%   }
%   \IntegrammeCategory{drink}{Drink}{
%     tea={Tea},coffee={Coffee},juice={Juice}
%   }
%   \IntegrammeCategory{pet}{Pet}{
%     cat={Cat},dog={Dog},rabbit={Rabbit}
%   }
% \end{integramme}
% \end{center}
%
% \section{Einstein's riddle}
%
% The well-known ``Einstein riddle'', also called the five-houses puzzle, is a
% natural stress test for a logic-grid package.  Its attribution to Albert
% Einstein is not established; the variant used here follows the data described
% by the French Wikipedia article ``Énigme d'Einstein'', consulted on
% 2026-08-23.  The clue wording below is independently paraphrased rather than
% copied from that page.
%
% \begin{quote}
% \small
% Reference:
% \href{https://fr.wikipedia.org/wiki/\%C3\%89nigme_d\%27Einstein}
%      {French Wikipedia -- Énigme d'Einstein}.
% \end{quote}
%
% Since version 0.8.1, the package includes this classic puzzle as a convenience
% preset.  The preset only declares ordinary categories, clues and solution
% rows; it does not introduce a different drawing engine or solving algorithm.
%
% \begin{verbatim}
% \begin{integramme}[
%   preset=einstein,
%   preset-language=fr
% ]
% \end{integramme}
% \end{verbatim}
%
% The same preset can be rendered in English with
% \texttt{preset-language=en}.  The default preset language is English.
% Passing \texttt{mode=solution} prints the complete solved grid.
%
% The full puzzle contains six categories of five items, including house
% position, so it is substantially larger than the quick-start example.  The
% following compact settings keep the demonstration inside the manual:
%
% \begin{center}
% \begin{integramme}[
%   preset=einstein,
%   preset-language=fr,
%   cell-size=3.3mm,
%   side-title-width=5.5mm,
%   side-label-min-width=18mm,
%   font=\scriptsize\sffamily,
%   title-font=\scriptsize\sffamily\bfseries,
%   indices-font=\footnotesize,
%   overflow-warning=false
% ]
% \end{integramme}
% \end{center}
%
% In this variant the German owns the fish.  A standalone, more generously
% spaced version is shipped as \texttt{example-einstein.tex}.
%
% \section{Categories and items}
%
% \begin{function}{\IntegrammeCategory}
%   \begin{syntax}
%     \cs{IntegrammeCategory}\marg{category id}\marg{printed title}\marg{items}
%   \end{syntax}
% \end{function}
%
% New documents should give every item an internal identifier and a printed
% label:
%
% \begin{verbatim}
% \IntegrammeCategory{city}{City}{
%   st_etienne={Saint-Étienne},
%   calais={Calais},
%   frejus={Fréjus}
% }
% \end{verbatim}
%
% Internal identifiers are used by manual marks and solutions.  Printed labels
% are independent and may contain commas, slashes, mathematics and ordinary
% formatting commands.  For maximum robustness, keep identifiers short and
% ASCII-like and avoid commas and equals signs in identifiers.
%
% The older shorthand
%
% \begin{verbatim}
% \IntegrammeCategory{city}{City}{Calais,Fréjus,Annecy}
% \end{verbatim}
%
% remains accepted for compatibility; in that form the identifier and printed
% label are the same token list.
%
% \section{Clues}
%
% \begin{function}{\IntegrammeIndices}
%   \begin{syntax}
%     \cs{IntegrammeIndices}\marg{item list}
%   \end{syntax}
% \end{function}
%
% Clues are written as ordinary \cs{item} entries.  They are printed as a
% numbered list below the grid.
%
% \begin{verbatim}
% \IntegrammeIndices{%
%   \item Alice does not drink tea.
%   \item The owner of the rabbit drinks coffee.
%   \item Clara is not the owner of the dog.
% }
% \end{verbatim}
%
% Because the argument contains normal list items, clue text may contain commas,
% semicolons and mathematics without introducing a package-level separator.
%
% \section{Modes}
%
% The key \texttt{mode} selects what is printed in the cells.
%
% \begin{description}[style=nextline]
% \item[\texttt{mode=blank}] Prints an empty pupil grid.  This is the default.
% \item[\texttt{mode=manual}] Prints only marks explicitly declared with
%   \cs{IntegrammeYes} and \cs{IntegrammeNo}.
% \item[\texttt{mode=solution}] Prints a complete positive/negative grid derived
%   from declared solution rows.
% \end{description}
%
% \subsection{Manual marks}
%
% \begin{function}{\IntegrammeYes,\IntegrammeNo}
%   \begin{syntax}
%     \cs{IntegrammeYes}\marg{category=item}\marg{category=item}
%     \cs{IntegrammeNo}\marg{category=item}\marg{category=item}
%   \end{syntax}
% \end{function}
%
% Example:
%
% \begin{verbatim}
% \begin{integramme}[mode=manual]
%   ... categories ...
%   \IntegrammeNo {person=alice}{drink=tea}
%   \IntegrammeYes{person=bruno}{pet=dog}
% \end{integramme}
% \end{verbatim}
%
% The two references may be supplied in either order.  The package reports an
% error if a reference is unknown, compares two items from the same category, or
% declares the same cell both positive and negative.
%
% \subsection{Complete solutions}
%
% \begin{function}{\IntegrammeSolutionRow}
%   \begin{syntax}
%     \cs{IntegrammeSolutionRow}\marg{category=item,...}
%   \end{syntax}
% \end{function}
%
% The recommended solution syntax is keyed and order-independent:
%
% \begin{verbatim}
% \begin{integramme}[mode=solution]
%   ... categories ...
%   \IntegrammeSolutionRow{
%     person=alice, drink=tea, pet=cat
%   }
%   \IntegrammeSolutionRow{
%     pet=dog, person=bruno, drink=coffee
%   }
%   \IntegrammeSolutionRow{
%     drink=juice, pet=rabbit, person=clara
%   }
% \end{integramme}
% \end{verbatim}
%
% Every solution row must assign exactly one item from every category.  A
% complete solution must contain exactly as many rows as there are items in each
% category, and every item must occur exactly once per category.
%
% \section{Configuration}
%
% Package settings can be set globally with \cs{integrammeset} and overridden
% locally in the optional argument of the environment.
%
% \begin{verbatim}
% \integrammeset{
%   cell-size=6mm,
%   column-labels=auto,
%   top-angle=45
% }
%
% \begin{integramme}[cell-size=5mm]
%   ...
% \end{integramme}
% \end{verbatim}
%
% \subsection{Geometry and rules}
%
% \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}}
% \toprule
% Key & \normalfont Default & Meaning \\
% \midrule
% cell-size & 6mm & \normalfont Width and height of one logical cell. \\
% rule-width & 0.3pt & \normalfont Width of thin internal grid lines. \\
% block-rule-width & 1pt & \normalfont Width of category/block borders. \\
% side-title-width & 7mm & \normalfont Width reserved for vertical category titles. \\
% side-label-width & 0pt & \normalfont Fixed row-label width; \texttt{0pt} requests automatic measurement. \\
% side-label-min-width & 26mm & \normalfont Minimum automatic row-label width. \\
% side-padding & 1.5mm & \normalfont Horizontal padding inside the row-label area. \\
% top-padding & 1mm & \normalfont Padding used in the top-label area. \\
% top-label-min-height & 7mm & \normalfont Minimum height of the top item-label area. \\
% title-height & 7mm & \normalfont Height of the top category-title row. \\
% indices-sep & 3mm & \normalfont Space between the grid and the clue list. \\
% guide-stem-height & 0pt & \normalfont Vertical stem before a slanted guide; \texttt{0pt} means automatic. \\
% \bottomrule
% \end{tabularx}
%
% \subsection{Header behaviour}
%
% \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}}
% \toprule
% Key & \normalfont Default & Meaning \\
% \midrule
% column-labels & auto & \normalfont \texttt{auto}, \texttt{horizontal} or \texttt{slanted}. In \texttt{auto}, all top labels become slanted if at least one does not fit horizontally. \\
% top-angle & 45 & \normalfont Angle, in degrees, used for slanted top labels. \\
% guides & true & \normalfont Shows or hides the top label guides. \\
% guide-length & 1.15 & \normalfont Slanted guide length, measured in multiples of \texttt{cell-size}. \\
% guide-opacity & 0.45 & \normalfont Opacity of the top guides. \\
% \bottomrule
% \end{tabularx}
%
% \subsection{Typography, colours and marks}
%
% \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}}
% \toprule
% Key & \normalfont Default & Meaning \\
% \midrule
% font & \cs{sffamily} & \normalfont Font commands used for ordinary item labels. \\
% title-font & \cs{sffamily}\cs{bfseries} & \normalfont Font commands used for category titles. \\
% indices-font & \cs{normalfont} & \normalfont Font commands used for the clue list. \\
% mark-font & \cs{sffamily} & \normalfont Font commands used for positive/negative marks. \\
% yes-mark & \cs{bullet} & \normalfont Material printed for a positive cell (math-safe by default). \\
% no-mark & \cs{times} & \normalfont Material printed for a negative cell (math-safe by default). \\
% grid-color & black & \normalfont TikZ colour used for the grid. \\
% text-color & black & \normalfont TikZ colour used for labels and marks. \\
% \bottomrule
% \end{tabularx}
%
% \subsection{Behaviour}
%
% \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}}
% \toprule
% Key & \normalfont Default & Meaning \\
% \midrule
% mode & blank & \normalfont \texttt{blank}, \texttt{manual} or \texttt{solution}; selects cell content. \\
% preset & none & \normalfont \texttt{none} or \texttt{einstein}; preloads a complete built-in puzzle data set. \\
% preset-language & en & \normalfont \texttt{en} or \texttt{fr}; language used by built-in preset labels and clues. \\
% overflow-warning & true & \normalfont Warns when the complete puzzle is wider than the current \cs{linewidth}. \\
% \bottomrule
% \end{tabularx}
%
% The package never scales a puzzle silently.  If the width warning fires,
% authors remain in control of cell size, label width and surrounding layout.
%
% \section{Compatibility syntax}
%
% The following forms are still accepted but are not recommended for new
% documents:
%
% \begin{verbatim}
% \IntegrammeCategory{person}{Person}{Alice,Bruno,Clara}
% \IntegrammeNo{person/Alice}{pet/Cat}
% \IntegrammeSolutionRow{Alice,Tea,Cat}
% \IntegrammeSolution{Alice/Tea/Cat;Bruno/Coffee/Dog;Clara/Juice/Rabbit}
% \end{verbatim}
%
% Short aliases beginning with \cs{Int} also remain available for compatibility
% and convenience, but the long-form commands are the candidate stable API.
%
% \section{Diagnostics and current limitations}
%
% Version 0.7.0 and later validate the data before drawing.  Among other checks, it reports
% duplicate category or item identifiers, unequal category sizes, malformed or
% unknown references, conflicting manual marks, incomplete solution rows and
% duplicate use of solution items.  Category identifiers are trimmed before
% storage, an empty category identifier is rejected explicitly, and a
% non-positive \texttt{cell-size} is rejected before the layout code can divide
% by it.
%
% Current limitations are:
% \begin{itemize}
% \item all categories must have the same number of items;
% \item only the staircase layout is implemented;
% \item long slanted top labels are not automatically constrained to their own
%       category block;
% \item identifiers should remain simple and should avoid commas and equals
%       signs.
% \end{itemize}
%
% \section{Regression testing}
%
% Version 0.9.0 runs the release-candidate regression suite on pdfTeX, LuaTeX and XeTeX.  The suite
% checks the candidate public surface and defaults in addition to canonical and
% compatibility syntax, global/local option scoping, rich labels, representative
% puzzle sizes, warnings and every package-defined error path.  Diagnostic tests
% redirect package errors to warnings so that complete transcripts can be
% compared by \texttt{l3build} without stopping the test run.
%
% \section{Building the package}
%
% The source tree now follows the conventional DocStrip/\texttt{l3build}
% pattern.  Useful commands are:
%
% \begin{verbatim}
% latex integramme.ins    # generate integramme.sty
% l3build unpack          # generate installable files in the build tree
% l3build doc             # build integramme.pdf
% l3build check           # run regression tests
% l3build ctan            # build a CTAN-style archive
% \end{verbatim}
%
% The release archive produced by \texttt{l3build ctan} is intended as the
% basis for a future CTAN submission.  Version 0.9.0 also ships a maintainer-side
% static audit in the development tree; it checks version consistency, the
% documented key/command surface and archive contents.  Maintainer metadata is
% finalized; optional repository and support URLs are omitted rather than invented.
%
% \section{Release metadata}
%
% CTAN requires a public maintainer name for a submission.  Jonathan LAFARGE
% is the public author, copyright holder and CTAN maintainer.  This work keeps
% LPPL status \texttt{author-}\allowbreak\texttt{maintained}, which reserves
% maintenance to the copyright holder.  Repository, bug-tracker and support
% URLs are optional CTAN communication metadata and are intentionally omitted
% rather than invented.  No end-user command depends on these fields.
%
% \section{Authorship and AI-assisted development}
%
% The author and copyright holder of \textsf{integramme} is Jonathan LAFARGE.
% OpenAI's ChatGPT was used as an interactive development assistant for code
% drafting, refactoring, test generation and the drafting/revision of
% documentation.  The model used for the current development cycle was
% \textbf{GPT-5.6 Sol} (August 2026).  Design decisions, review, testing and
% release responsibility remain with the human author.
%
% ChatGPT is therefore acknowledged as a development tool, not listed as an
% author, copyright holder or CTAN maintainer.
%
% \section{License}
%
% This work may be distributed and/or modified under the conditions of the
% LaTeX Project Public License, version 1.3c or later.  The latest version of
% this license is available from \texttt{https://www.latex-project.org/lppl/}.
% The work has LPPL maintenance status \texttt{author-maintained}.
%
% \StopEventually{}
%
% \section{Implementation}
%
% The implementation remains based on LaTeX3 data structures and TikZ.  Version
% 0.9.0 is the release-candidate freeze: the ordinary rendering algorithm and
% public long-form API are unchanged from the audited 0.8 series.
%
% \begin{macrocode}
%<*package>
\NeedsTeXFormat{LaTeX2e}[2022-06-01]
\ProvidesExplPackage
  {integramme}
  {2026-08-23}
  {0.9.0}
  {Typeset logic-grid puzzles (integrammes)}

\RequirePackage{tikz}

\ExplSyntaxOn
\cs_generate_variant:Nn \prop_item:Nn { Ne }

% --------------------------------------------------------------------------
% Data
% --------------------------------------------------------------------------
\seq_new:N  \l__integramme_categories_seq
\prop_new:N \l__integramme_titles_prop
\prop_new:N \l__integramme_item_count_prop
\prop_new:N \l__integramme_item_id_prop
\prop_new:N \l__integramme_item_label_prop
\prop_new:N \l__integramme_item_index_prop
\seq_new:N  \l__integramme_solution_seq % normalized rows, category order
\seq_new:N  \l__integramme_solution_input_seq % raw rows as entered by the user
\seq_new:N  \l__integramme_manual_yes_seq
\seq_new:N  \l__integramme_manual_no_seq
\prop_new:N \l__integramme_manual_marks_prop
\prop_new:N \l__integramme_solution_row_prop
\tl_new:N   \l__integramme_indices_tl
\tl_new:N   \l__integramme_current_category_tl

\int_new:N  \l__integramme_category_count_int
\int_new:N  \l__integramme_order_int
\int_new:N  \l__integramme_scan_int
\int_new:N  \l__integramme_current_item_int
\int_new:N  \l__integramme_draw_row_int
\int_new:N  \l__integramme_refa_cat_int
\int_new:N  \l__integramme_refa_item_int
\int_new:N  \l__integramme_refb_cat_int
\int_new:N  \l__integramme_refb_item_int
\bool_new:N \l__integramme_valid_bool

% --------------------------------------------------------------------------
% Configuration
% --------------------------------------------------------------------------
\dim_new:N \l__integramme_cell_dim
\dim_new:N \l__integramme_rule_dim
\dim_new:N \l__integramme_block_rule_dim
\dim_new:N \l__integramme_side_title_dim
\dim_new:N \l__integramme_side_label_dim
\dim_new:N \l__integramme_side_label_min_dim
\dim_new:N \l__integramme_side_padding_dim
\dim_new:N \l__integramme_top_padding_dim
\dim_new:N \l__integramme_top_label_min_dim
\dim_new:N \l__integramme_top_label_dim
\dim_new:N \l__integramme_title_height_dim
\dim_new:N \l__integramme_indices_sep_dim
\dim_new:N \l__integramme_guide_stem_dim
\dim_new:N \l__integramme_guide_stem_effective_dim
\dim_new:N \l__integramme_total_width_dim

\tl_new:N  \l__integramme_font_tl
\tl_new:N  \l__integramme_title_font_tl
\tl_new:N  \l__integramme_mode_tl
\tl_new:N  \l__integramme_yes_tl
\tl_new:N  \l__integramme_no_tl
\tl_new:N  \l__integramme_grid_color_tl
\tl_new:N  \l__integramme_text_color_tl
\tl_new:N  \l__integramme_column_labels_tl
\tl_new:N  \l__integramme_indices_font_tl
\tl_new:N  \l__integramme_mark_font_tl
\tl_new:N  \l__integramme_preset_tl
\tl_new:N  \l__integramme_preset_language_tl

\fp_new:N  \l__integramme_top_angle_fp
\fp_new:N  \l__integramme_guide_length_fp
\fp_new:N  \l__integramme_guide_opacity_fp

\bool_new:N \l__integramme_top_slanted_bool
\bool_new:N \l__integramme_guides_bool
\bool_new:N \l__integramme_overflow_warning_bool

\keys_define:nn { integramme }
  {
    cell-size .dim_set:N = \l__integramme_cell_dim,
    cell-size .initial:n = 6mm,

    rule-width .dim_set:N = \l__integramme_rule_dim,
    rule-width .initial:n = 0.3pt,

    block-rule-width .dim_set:N = \l__integramme_block_rule_dim,
    block-rule-width .initial:n = 1pt,

    side-title-width .dim_set:N = \l__integramme_side_title_dim,
    side-title-width .initial:n = 7mm,

    side-label-width .dim_set:N = \l__integramme_side_label_dim,
    side-label-width .initial:n = 0pt,

    side-label-min-width .dim_set:N = \l__integramme_side_label_min_dim,
    side-label-min-width .initial:n = 26mm,

    side-padding .dim_set:N = \l__integramme_side_padding_dim,
    side-padding .initial:n = 1.5mm,

    top-padding .dim_set:N = \l__integramme_top_padding_dim,
    top-padding .initial:n = 1mm,

    top-label-min-height .dim_set:N = \l__integramme_top_label_min_dim,
    top-label-min-height .initial:n = 7mm,

    title-height .dim_set:N = \l__integramme_title_height_dim,
    title-height .initial:n = 7mm,

    indices-sep .dim_set:N = \l__integramme_indices_sep_dim,
    indices-sep .initial:n = 3mm,

    top-angle .fp_set:N = \l__integramme_top_angle_fp,
    top-angle .initial:n = 45,

    guide-length .fp_set:N = \l__integramme_guide_length_fp,
    guide-length .initial:n = 1.15,

    % Height of the short vertical part before a slanted separator turns.
    % 0pt means automatic: approximately one capital-letter height.
    guide-stem-height .dim_set:N = \l__integramme_guide_stem_dim,
    guide-stem-height .initial:n = 0pt,

    guide-opacity .fp_set:N = \l__integramme_guide_opacity_fp,
    guide-opacity .initial:n = 0.45,

    guides .bool_set:N = \l__integramme_guides_bool,
    guides .initial:n = true,

    overflow-warning .bool_set:N = \l__integramme_overflow_warning_bool,
    overflow-warning .initial:n = true,

    font .tl_set:N = \l__integramme_font_tl,
    font .initial:n = \sffamily,

    title-font .tl_set:N = \l__integramme_title_font_tl,
    title-font .initial:n = \sffamily\bfseries,

    indices-font .tl_set:N = \l__integramme_indices_font_tl,
    indices-font .initial:n = \normalfont,

    mark-font .tl_set:N = \l__integramme_mark_font_tl,
    mark-font .initial:n = \sffamily,

    grid-color .tl_set:N = \l__integramme_grid_color_tl,
    grid-color .initial:n = black,

    text-color .tl_set:N = \l__integramme_text_color_tl,
    text-color .initial:n = black,

    yes-mark .tl_set:N = \l__integramme_yes_tl,
    yes-mark .initial:n = \ensuremath{\bullet},

    no-mark .tl_set:N = \l__integramme_no_tl,
    no-mark .initial:n = \ensuremath{\times},

    column-labels .choice:,
    column-labels / auto .code:n =
      { \tl_set:Nn \l__integramme_column_labels_tl { auto } },
    column-labels / horizontal .code:n =
      { \tl_set:Nn \l__integramme_column_labels_tl { horizontal } },
    column-labels / slanted .code:n =
      { \tl_set:Nn \l__integramme_column_labels_tl { slanted } },
    column-labels .initial:n = auto,

    preset .choice:,
    preset / none .code:n =
      { \tl_set:Nn \l__integramme_preset_tl { none } },
    preset / einstein .code:n =
      { \tl_set:Nn \l__integramme_preset_tl { einstein } },
    preset .initial:n = none,

    preset-language .choice:,
    preset-language / en .code:n =
      { \tl_set:Nn \l__integramme_preset_language_tl { en } },
    preset-language / fr .code:n =
      { \tl_set:Nn \l__integramme_preset_language_tl { fr } },
    preset-language .initial:n = en,

    mode .choice:,
    mode / blank .code:n = { \tl_set:Nn \l__integramme_mode_tl { blank } },
    mode / manual .code:n = { \tl_set:Nn \l__integramme_mode_tl { manual } },
    mode / solution .code:n = { \tl_set:Nn \l__integramme_mode_tl { solution } },
    mode .initial:n = blank,
  }

\NewDocumentCommand \integrammeset { m }
  { \keys_set:nn { integramme } {#1} }

% --------------------------------------------------------------------------
% Messages
% --------------------------------------------------------------------------
\msg_new:nnn { integramme } { duplicate-category }
  { The~category~identifier~'#1'~is~already~used. }

\msg_new:nnn { integramme } { empty-category-id }
  { A~category~must~have~a~non-empty~identifier. }

\msg_new:nnn { integramme } { duplicate-item }
  { The~item~identifier~'#1'~is~already~used~in~category~'#2'. }

\msg_new:nnn { integramme } { empty-item-id }
  { Category~'#1'~contains~an~item~with~an~empty~identifier. }

\msg_new:nnn { integramme } { empty-category }
  { Category~'#1'~must~contain~at~least~one~item. }

\msg_new:nnn { integramme } { too-few-categories }
  { An~integramme~needs~at~least~two~categories. }

\msg_new:nnn { integramme } { unequal-sizes }
  { All~categories~must~have~the~same~number~of~items.~Category~'#1'~does~not. }

\msg_new:nnn { integramme } { missing-solution }
  { mode=solution~was~requested,~but~no~solution~was~declared. }

\msg_new:nnn { integramme } { bad-solution-size }
  { The~solution~must~contain~exactly~#1~rows. }

\msg_new:nnn { integramme } { bad-solution-row }
  { Each~positional~solution~row~must~contain~exactly~#1~entries. }

\msg_new:nnn { integramme } { bad-keyed-solution-entry }
  { Solution~row~entry~'#1'~must~have~the~form~category=item. }

\msg_new:nnn { integramme } { duplicate-solution-category }
  { Category~'#1'~is~assigned~more~than~once~in~the~same~solution~row. }

\msg_new:nnn { integramme } { missing-solution-category }
  { Solution~row~does~not~assign~an~item~to~category~'#1'. }

\msg_new:nnn { integramme } { duplicate-solution-item }
  { Solution~item~'#2'~occurs~more~than~once~in~category~'#1'. }

\msg_new:nnn { integramme } { grid-too-wide }
  { The~integramme~is~#1~wide,~but~the~current~line~width~is~#2. }

\msg_new:nnn { integramme } { nonpositive-cell-size }
  { The~key~'cell-size'~must~be~greater~than~0pt. }

\msg_new:nnn { integramme } { bad-reference }
  { The~reference~'#1'~must~have~the~form~category=item~(or~legacy~category/item). }

\msg_new:nnn { integramme } { unknown-category }
  { The~category~identifier~'#1'~does~not~exist. }

\msg_new:nnn { integramme } { unknown-item }
  { The~item~'#2'~does~not~exist~in~category~'#1'. }

\msg_new:nnn { integramme } { same-category-mark }
  { A~mark~cannot~compare~two~items~from~the~same~category~('#1'). }

\msg_new:nnn { integramme } { conflicting-mark }
  { The~cell~'#1'~has~been~declared~both~positive~and~negative. }

\msg_new:nnn { integramme } { manual-marks-ignored }
  { Manual~marks~were~declared,~but~mode=#1.~They~will~not~be~printed. }

% --------------------------------------------------------------------------
% User data commands
% --------------------------------------------------------------------------
% Since 0.4, item identifiers are separated from printed labels.
% The third argument of \IntegrammeCategory is parsed as a key-value list:
%   Alice, Irene                       -> legacy shorthand (id = label)
%   alice={Alice}, irene={Irène}       -> robust form (recommended)
% Braces protect commas, slashes, mathematics and arbitrary LaTeX in labels.

\cs_new:Npn \__integramme_item_key:nn #1#2 { #1 || #2 }
\cs_new:Npn \__integramme_index_key:nn #1#2 { #1 / \int_eval:n {#2} }

\cs_new_protected:Npn \__integramme_store_item:nn #1#2
  {
    \tl_set:Nn \l_tmpa_tl {#1}
    \tl_trim_spaces:N \l_tmpa_tl
    \tl_set:Nn \l_tmpb_tl {#2}
    \tl_trim_spaces:N \l_tmpb_tl

    \tl_if_empty:NTF \l_tmpa_tl
      {
        \msg_error:nnx { integramme } { empty-item-id }
          { \tl_use:N \l__integramme_current_category_tl }
        \bool_set_false:N \l__integramme_valid_bool
      }
      {
        \tl_set:Nx \l_tmpc_tl
          {
            \__integramme_item_key:nn
              { \tl_use:N \l__integramme_current_category_tl }
              { \tl_use:N \l_tmpa_tl }
          }
        \prop_if_in:NVTF \l__integramme_item_index_prop \l_tmpc_tl
          {
            \msg_error:nnxx { integramme } { duplicate-item }
              { \tl_use:N \l_tmpa_tl }
              { \tl_use:N \l__integramme_current_category_tl }
            \bool_set_false:N \l__integramme_valid_bool
          }
          {
            \int_incr:N \l__integramme_current_item_int
            \prop_put:NVx \l__integramme_item_index_prop \l_tmpc_tl
              { \int_use:N \l__integramme_current_item_int }

            \tl_set:Nx \l_tmpc_tl
              {
                \__integramme_index_key:nn
                  { \tl_use:N \l__integramme_current_category_tl }
                  { \int_use:N \l__integramme_current_item_int }
              }
            \prop_put:NVV \l__integramme_item_id_prop \l_tmpc_tl \l_tmpa_tl
            \prop_put:NVV \l__integramme_item_label_prop \l_tmpc_tl \l_tmpb_tl
          }
      }
  }

\cs_new_protected:Npn \__integramme_item_bare:n #1
  { \__integramme_store_item:nn {#1}{#1} }

\cs_new_protected:Npn \__integramme_item_keyval:nn #1#2
  { \__integramme_store_item:nn {#1}{#2} }

\cs_new_protected:Npn \__integramme_add_category:nnn #1#2#3
  {
    % Category identifiers are structural data, so insignificant surrounding
    % spaces are normalized before the identifier is stored or compared.
    \tl_set:Nn \l_tmpa_tl {#1}
    \tl_trim_spaces:N \l_tmpa_tl
    \tl_if_empty:NTF \l_tmpa_tl
      {
        \msg_error:nn { integramme } { empty-category-id }
        \bool_set_false:N \l__integramme_valid_bool
      }
      {
        \seq_if_in:NVTF \l__integramme_categories_seq \l_tmpa_tl
          {
            \msg_error:nnx { integramme } { duplicate-category }
              { \tl_use:N \l_tmpa_tl }
            \bool_set_false:N \l__integramme_valid_bool
          }
          {
            \seq_put_right:NV \l__integramme_categories_seq \l_tmpa_tl
            \prop_put:NVn \l__integramme_titles_prop \l_tmpa_tl {#2}
            \tl_set:Nx \l__integramme_current_category_tl { \tl_use:N \l_tmpa_tl }
            \int_zero:N \l__integramme_current_item_int
            % The kernel key-value parser reports a generic error for an empty
            % left-hand key before our callbacks run.  Detect that case first so
            % users receive an integramme-specific diagnostic instead.
            \regex_match:nnTF { (?: \A | , ) \s* = } {#3}
              {
                \msg_error:nnx { integramme } { empty-item-id }
                  { \tl_use:N \l__integramme_current_category_tl }
                \bool_set_false:N \l__integramme_valid_bool
              }
              {
                \keyval_parse:NNn
                  \__integramme_item_bare:n
                  \__integramme_item_keyval:nn
                  {#3}
              }
            \prop_put:NVx \l__integramme_item_count_prop \l__integramme_current_category_tl
              { \int_use:N \l__integramme_current_item_int }
          }
      }
  }

\NewDocumentCommand \IntegrammeCategory { m m m }
  { \__integramme_add_category:nnn {#1}{#2}{#3} }
\cs_new_eq:NN \IntCategory \IntegrammeCategory

% Version 0.5 stabilizes the public data API around key-value input.
%
% Recommended solution syntax (order-independent):
%   \IntegrammeSolutionRow{person=alice,city=calais,age=a26}
%
% For backward compatibility, positional rows such as
%   \IntegrammeSolutionRow{alice,calais,a26}
% and the legacy compact \IntegrammeSolution command are still accepted.
% Raw rows are normalized only when the environment closes, so declarations
% remain order-flexible.
\NewDocumentCommand \IntegrammeSolution { m }
  {
    \seq_clear:N \l__integramme_solution_input_seq
    \seq_set_split:Nnn \l_tmpa_seq { ; } {#1}
    \seq_map_inline:Nn \l_tmpa_seq
      {
        \seq_set_split:Nnn \l_tmpb_seq {/} {##1}
        \tl_set:Nx \l_tmpa_tl { \seq_use:Nn \l_tmpb_seq {,} }
        \seq_put_right:NV \l__integramme_solution_input_seq \l_tmpa_tl
      }
  }
\cs_new_eq:NN \IntSolution \IntegrammeSolution

\NewDocumentCommand \IntegrammeSolutionRow { m }
  { \seq_put_right:Nn \l__integramme_solution_input_seq {#1} }
\cs_new_eq:NN \IntSolutionRow \IntegrammeSolutionRow

% The argument contains ordinary \item commands. Using LaTeX list syntax here
% avoids reserving punctuation characters inside clues.
\NewDocumentCommand \IntegrammeIndices { +m }
  { \tl_set:Nn \l__integramme_indices_tl {#1} }
\cs_new_eq:NN \IntIndices \IntegrammeIndices

% Recommended 0.5 manual-mark syntax keeps two explicit references:
%   \IntegrammeYes{person=alice}{age=a26}
%   \IntegrammeNo {person=alice}{city=annecy}
%
% The 0.3/0.4 category/item references remain accepted for compatibility.
% Marks are resolved only when the environment closes, keeping input order
% flexible.
\NewDocumentCommand \IntegrammeYes { m m }
  { \seq_put_right:Nn \l__integramme_manual_yes_seq { {#1}{#2} } }
\cs_new_eq:NN \IntYes \IntegrammeYes

\NewDocumentCommand \IntegrammeNo { m m }
  { \seq_put_right:Nn \l__integramme_manual_no_seq { {#1}{#2} } }
\cs_new_eq:NN \IntNo \IntegrammeNo

% --------------------------------------------------------------------------
% Built-in presets
% --------------------------------------------------------------------------
% Presets are convenience data sets. They deliberately use only the public
% declaration commands, so preset puzzles exercise the same validation and
% drawing paths as user-authored puzzles.

\cs_new_protected:Npn \__integramme_preset_einstein_solution:
  {
    \IntegrammeSolutionRow
      { nationality=norwegian,house=h1,color=yellow,drink=water,cigar=dunhill,pet=cats }
    \IntegrammeSolutionRow
      { nationality=dane,house=h2,color=blue,drink=tea,cigar=blend,pet=horse }
    \IntegrammeSolutionRow
      { nationality=briton,house=h3,color=red,drink=milk,cigar=pallmall,pet=birds }
    \IntegrammeSolutionRow
      { nationality=german,house=h4,color=green,drink=coffee,cigar=prince,pet=fish }
    \IntegrammeSolutionRow
      { nationality=swede,house=h5,color=white,drink=beer,cigar=bluemaster,pet=dogs }
  }

\cs_new_protected:Npn \__integramme_preset_einstein_en:
  {
    \IntegrammeCategory { nationality } { Nationality }
      { briton={Briton},swede={Swede},dane={Dane},norwegian={Norwegian},german={German} }
    \IntegrammeCategory { house } { House }
      { h1={House~1},h2={House~2},h3={House~3},h4={House~4},h5={House~5} }
    \IntegrammeCategory { color } { Color }
      { red={Red},green={Green},white={White},yellow={Yellow},blue={Blue} }
    \IntegrammeCategory { drink } { Drink }
      { tea={Tea},coffee={Coffee},milk={Milk},beer={Beer},water={Water} }
    \IntegrammeCategory { cigar } { Cigar }
      { pallmall={Pall~Mall},dunhill={Dunhill},blend={Blend},bluemaster={Bluemaster},prince={Prince} }
    \IntegrammeCategory { pet } { Pet }
      { dogs={Dogs},birds={Birds},cats={Cats},horse={Horse},fish={Fish} }
    \IntegrammeIndices
      {
        \item The~Briton~lives~in~the~red~house.
        \item The~Swede~owns~dogs.
        \item The~Dane~drinks~tea.
        \item The~green~house~is~immediately~to~the~left~of~the~white~house.
        \item Coffee~is~drunk~in~the~green~house.
        \item The~Pall~Mall~smoker~keeps~birds.
        \item Dunhill~is~smoked~in~the~yellow~house.
        \item Milk~is~drunk~in~the~middle~house.
        \item The~Norwegian~lives~in~the~first~house~from~the~left.
        \item The~Blend~smoker~lives~next~to~the~cat~owner.
        \item The~horse~owner~lives~next~to~the~Dunhill~smoker.
        \item The~Bluemaster~smoker~drinks~beer.
        \item The~German~smokes~Prince.
        \item The~Norwegian~lives~next~to~the~blue~house.
        \item The~Blend~smoker~has~a~neighbour~who~drinks~water.
        \item[\textbf{Question:}] Who~owns~the~fish?
      }
    \__integramme_preset_einstein_solution:
  }

\cs_new_protected:Npn \__integramme_preset_einstein_fr:
  {
    \IntegrammeCategory { nationality } { Nationalité }
      { briton={Britannique},swede={Suédois},dane={Danois},norwegian={Norvégien},german={Allemand} }
    \IntegrammeCategory { house } { Maison }
      { h1={Maison~1},h2={Maison~2},h3={Maison~3},h4={Maison~4},h5={Maison~5} }
    \IntegrammeCategory { color } { Couleur }
      { red={Rouge},green={Verte},white={Blanche},yellow={Jaune},blue={Bleue} }
    \IntegrammeCategory { drink } { Boisson }
      { tea={Thé},coffee={Café},milk={Lait},beer={Bière},water={Eau} }
    \IntegrammeCategory { cigar } { Cigare }
      { pallmall={Pall~Mall},dunhill={Dunhill},blend={Blend},bluemaster={Bluemaster},prince={Prince} }
    \IntegrammeCategory { pet } { Animal }
      { dogs={Chiens},birds={Oiseaux},cats={Chats},horse={Cheval},fish={Poisson rouge} }
    \IntegrammeIndices
      {
        \item La~maison~rouge~est~celle~du~Britannique.
        \item Le~Suédois~possède~des~chiens.
        \item Le~Danois~boit~du~thé.
        \item La~maison~verte~se~trouve~immédiatement~à~gauche~de~la~maison~blanche.
        \item Dans~la~maison~verte,~on~boit~du~café.
        \item La~personne~qui~fume~des~Pall~Mall~possède~des~oiseaux.
        \item Les~Dunhill~sont~fumées~dans~la~maison~jaune.
        \item Du~lait~est~bu~dans~la~troisième~maison.
        \item Le~Norvégien~occupe~la~première~maison,~à~l'extrémité~gauche.
        \item Le~fumeur~de~Blend~habite~à~côté~de~la~personne~qui~possède~des~chats.
        \item La~personne~qui~possède~un~cheval~vit~à~côté~du~fumeur~de~Dunhill.
        \item Le~fumeur~de~Bluemaster~boit~de~la~bière.
        \item L'Allemand~fume~des~Prince.
        \item La~maison~du~Norvégien~est~voisine~de~la~maison~bleue.
        \item Le~fumeur~de~Blend~a~pour~voisin~quelqu'un~qui~boit~de~l'eau.
        \item[\textbf{Question~:}] Qui~possède~le~poisson~rouge~?
      }
    \__integramme_preset_einstein_solution:
  }

\cs_new_protected:Npn \__integramme_apply_preset:
  {
    \tl_if_eq:NnT \l__integramme_preset_tl { einstein }
      {
        \tl_if_eq:NnTF \l__integramme_preset_language_tl { fr }
          { \__integramme_preset_einstein_fr: }
          { \__integramme_preset_einstein_en: }
      }
  }

% --------------------------------------------------------------------------
% Manual-mark references
% --------------------------------------------------------------------------
\cs_new_protected:Npn \__integramme_resolve_ids:nnNN #1#2#3#4
  {
    \int_zero:N #3
    \int_zero:N #4
    \tl_set:Nn \l_tmpa_tl {#1}
    \tl_set:Nn \l_tmpb_tl {#2}
    \tl_trim_spaces:N \l_tmpa_tl
    \tl_trim_spaces:N \l_tmpb_tl

    \int_zero:N \l__integramme_scan_int
    \seq_map_inline:Nn \l__integramme_categories_seq
      {
        \int_incr:N \l__integramme_scan_int
        \tl_set:Nn \l_tmpc_tl {##1}
        \tl_if_eq:NNT \l_tmpc_tl \l_tmpa_tl
          {
            \int_set_eq:NN #3 \l__integramme_scan_int
            \seq_map_break:
          }
      }

    \int_compare:nNnTF {#3} = {0}
      {
        \msg_error:nnx { integramme } { unknown-category }
          { \tl_use:N \l_tmpa_tl }
        \bool_set_false:N \l__integramme_valid_bool
      }
      {
        \tl_set:Nx \l_tmpc_tl
          {
            \__integramme_item_key:nn
              { \tl_use:N \l_tmpa_tl }
              { \tl_use:N \l_tmpb_tl }
          }
        \prop_get:NVNTF \l__integramme_item_index_prop \l_tmpc_tl \l_tmpd_tl
          { \int_set:Nn #4 { \l_tmpd_tl } }
          {
            \msg_error:nnxx { integramme } { unknown-item }
              { \tl_use:N \l_tmpa_tl }
              { \tl_use:N \l_tmpb_tl }
            \bool_set_false:N \l__integramme_valid_bool
          }
      }
  }

\cs_new_protected:Npn \__integramme_resolve_ref:nNN #1#2#3
  {
    \tl_if_in:nnTF {#1} {=}
      { \seq_set_split:Nnn \l_tmpa_seq {=} {#1} }
      { \seq_set_split:Nnn \l_tmpa_seq {/} {#1} }
    \int_compare:nNnTF { \seq_count:N \l_tmpa_seq } = { 2 }
      {
        \tl_set:Nx \l_tmpa_tl { \seq_item:Nn \l_tmpa_seq {1} }
        \tl_set:Nx \l_tmpb_tl { \seq_item:Nn \l_tmpa_seq {2} }
        \exp_args:NVV \__integramme_resolve_ids:nnNN
          \l_tmpa_tl \l_tmpb_tl #2 #3
      }
      {
        \int_zero:N #2
        \int_zero:N #3
        \msg_error:nnn { integramme } { bad-reference } {#1}
        \bool_set_false:N \l__integramme_valid_bool
      }
  }

\cs_new:Npn \__integramme_mark_key:nnnn #1#2#3#4
  {
    \int_compare:nNnTF {#1} < {#3}
      {
        \int_eval:n {#1}:\int_eval:n {#2}|
        \int_eval:n {#3}:\int_eval:n {#4}
      }
      {
        \int_eval:n {#3}:\int_eval:n {#4}|
        \int_eval:n {#1}:\int_eval:n {#2}
      }
  }

\cs_new_protected:Npn \__integramme_register_resolved_mark:nnnnn #1#2#3#4#5
  {
    \int_compare:nNnT {#2} > {0}
      {
        \int_compare:nNnT {#4} > {0}
          {
            \int_compare:nNnTF {#2} = {#4}
              {
                \msg_error:nnx { integramme } { same-category-mark }
                  { \__integramme_cat_id:n {#2} }
                \bool_set_false:N \l__integramme_valid_bool
              }
              {
                \tl_set:Nx \l_tmpa_tl
                  { \__integramme_mark_key:nnnn {#2}{#3}{#4}{#5} }
                \prop_get:NVNTF
                  \l__integramme_manual_marks_prop \l_tmpa_tl \l_tmpb_tl
                  {
                    \tl_if_eq:NnF \l_tmpb_tl {#1}
                      {
                        \msg_error:nnx { integramme } { conflicting-mark }
                          { \tl_use:N \l_tmpa_tl }
                        \bool_set_false:N \l__integramme_valid_bool
                      }
                  }
                  {
                    \prop_put:NVn \l__integramme_manual_marks_prop \l_tmpa_tl {#1}
                  }
              }
          }
      }
  }

\cs_new_protected:Npn \__integramme_register_manual_mark:nnn #1#2#3
  {
    \__integramme_resolve_ref:nNN
      {#2} \l__integramme_refa_cat_int \l__integramme_refa_item_int
    \__integramme_resolve_ref:nNN
      {#3} \l__integramme_refb_cat_int \l__integramme_refb_item_int
    \__integramme_register_resolved_mark:nnnnn
      {#1}
      {\l__integramme_refa_cat_int}{\l__integramme_refa_item_int}
      {\l__integramme_refb_cat_int}{\l__integramme_refb_item_int}
  }

\cs_new_protected:Npn \__integramme_resolve_manual_marks:
  {
    \prop_clear:N \l__integramme_manual_marks_prop
    \seq_map_inline:Nn \l__integramme_manual_yes_seq
      { \__integramme_register_manual_mark:nnn {yes} ##1 }
    \seq_map_inline:Nn \l__integramme_manual_no_seq
      { \__integramme_register_manual_mark:nnn {no} ##1 }

    \bool_lazy_or:nnT
      { ! \seq_if_empty_p:N \l__integramme_manual_yes_seq }
      { ! \seq_if_empty_p:N \l__integramme_manual_no_seq }
      {
        \tl_if_eq:NnF \l__integramme_mode_tl { manual }
          {
            \msg_warning:nnx { integramme } { manual-marks-ignored }
              { \tl_use:N \l__integramme_mode_tl }
          }
      }
  }

% --------------------------------------------------------------------------
% Validation
% --------------------------------------------------------------------------
\prop_new:N \l__integramme_solution_seen_prop

\cs_new_protected:Npn \__integramme_solution_row_bare:n #1
  {
    \msg_error:nnn { integramme } { bad-keyed-solution-entry } {#1}
    \bool_set_false:N \l__integramme_valid_bool
  }

\cs_new_protected:Npn \__integramme_solution_row_keyval:nn #1#2
  {
    \tl_set:Nn \l_tmpa_tl {#1}
    \tl_set:Nn \l_tmpb_tl {#2}
    \tl_trim_spaces:N \l_tmpa_tl
    \tl_trim_spaces:N \l_tmpb_tl

    \prop_if_in:NVTF \l__integramme_titles_prop \l_tmpa_tl
      {
        \prop_if_in:NVTF \l__integramme_solution_row_prop \l_tmpa_tl
          {
            \msg_error:nnx { integramme } { duplicate-solution-category }
              { \tl_use:N \l_tmpa_tl }
            \bool_set_false:N \l__integramme_valid_bool
          }
          {
            \prop_put:NVV
              \l__integramme_solution_row_prop \l_tmpa_tl \l_tmpb_tl
          }
      }
      {
        \msg_error:nnx { integramme } { unknown-category }
          { \tl_use:N \l_tmpa_tl }
        \bool_set_false:N \l__integramme_valid_bool
      }
  }

\cs_new_protected:Npn \__integramme_normalize_keyed_solution_row:n #1
  {
    \prop_clear:N \l__integramme_solution_row_prop
    \keyval_parse:NNn
      \__integramme_solution_row_bare:n
      \__integramme_solution_row_keyval:nn
      {#1}

    \clist_clear:N \l_tmpa_clist
    \seq_map_inline:Nn \l__integramme_categories_seq
      {
        \prop_get:NnNTF \l__integramme_solution_row_prop {##1} \l_tmpa_tl
          { \clist_put_right:NV \l_tmpa_clist \l_tmpa_tl }
          {
            \msg_error:nnn { integramme } { missing-solution-category } {##1}
            \bool_set_false:N \l__integramme_valid_bool
            \clist_put_right:Nn \l_tmpa_clist { __integramme_missing__ }
          }
      }
    \tl_set:Nx \l_tmpa_tl { \clist_use:Nn \l_tmpa_clist {,} }
    \seq_put_right:NV \l__integramme_solution_seq \l_tmpa_tl
  }

\cs_new_protected:Npn \__integramme_normalize_solution_inputs:
  {
    \seq_clear:N \l__integramme_solution_seq
    \seq_map_inline:Nn \l__integramme_solution_input_seq
      {
        \tl_if_in:nnTF {##1} {=}
          { \__integramme_normalize_keyed_solution_row:n {##1} }
          { \seq_put_right:Nn \l__integramme_solution_seq {##1} }
      }
  }

\cs_new_protected:Npn \__integramme_validate_solution_row:n #1
  {
    \clist_set:Nn \l_tmpa_clist {#1}
    \int_compare:nNnTF
      { \clist_count:N \l_tmpa_clist }
      =
      { \l__integramme_category_count_int }
      {
        \int_step_inline:nn { \l__integramme_category_count_int }
          {
            \tl_set:Nx \l_tmpa_tl
              { \clist_item:Nn \l_tmpa_clist {##1} }
            \tl_trim_spaces:N \l_tmpa_tl
            \tl_set:Nx \l_tmpb_tl { \__integramme_cat_id:n {##1} }
            \tl_if_eq:NnF \l_tmpa_tl { __integramme_missing__ }
              {
                \tl_set:Nx \l_tmpc_tl
                  {
                    \__integramme_item_key:nn
                      { \tl_use:N \l_tmpb_tl }
                      { \tl_use:N \l_tmpa_tl }
                  }
                \prop_if_in:NVTF \l__integramme_item_index_prop \l_tmpc_tl
                  {
                    \tl_set:Nx \l_tmpd_tl
                      { ##1 | \tl_use:N \l_tmpa_tl }
                    \prop_if_in:NVTF \l__integramme_solution_seen_prop \l_tmpd_tl
                      {
                        \msg_error:nnxx { integramme } { duplicate-solution-item }
                          { \tl_use:N \l_tmpb_tl }
                          { \tl_use:N \l_tmpa_tl }
                        \bool_set_false:N \l__integramme_valid_bool
                      }
                      {
                        \prop_put:NVn \l__integramme_solution_seen_prop \l_tmpd_tl {1}
                      }
                  }
                  {
                    \msg_error:nnxx { integramme } { unknown-item }
                      { \tl_use:N \l_tmpb_tl }
                      { \tl_use:N \l_tmpa_tl }
                    \bool_set_false:N \l__integramme_valid_bool
                  }
              }
          }
      }
      {
        \msg_error:nnx { integramme } { bad-solution-row }
          { \int_use:N \l__integramme_category_count_int }
        \bool_set_false:N \l__integramme_valid_bool
      }
  }

\cs_new_protected:Npn \__integramme_validate:
  {
    \int_set:Nn \l__integramme_category_count_int
      { \seq_count:N \l__integramme_categories_seq }

    % cell-size is used as a divisor by the layout code. Rejecting zero (and
    % negative values) here gives a package diagnostic instead of an opaque
    % floating-point/TikZ error later in the drawing stage.
    \dim_compare:nNnF { \l__integramme_cell_dim } > { 0pt }
      {
        \msg_error:nn { integramme } { nonpositive-cell-size }
        \bool_set_false:N \l__integramme_valid_bool
      }

    % Declaration-time errors (for example duplicate item identifiers) set the
    % same validity flag. Do not reset it here: that was a pre-0.7 bug which
    % could allow an invalid puzzle to reach the drawing stage.
    \bool_if:NT \l__integramme_valid_bool
      {
        \int_compare:nNnTF { \l__integramme_category_count_int } < { 2 }
          {
            \msg_error:nn { integramme } { too-few-categories }
            \bool_set_false:N \l__integramme_valid_bool
          }
          {
            \tl_set:Nx \l_tmpa_tl
              { \seq_item:Nn \l__integramme_categories_seq {1} }
            \prop_get:NVN \l__integramme_item_count_prop \l_tmpa_tl \l_tmpb_tl
            \int_set:Nn \l__integramme_order_int { \l_tmpb_tl }

            \seq_map_inline:Nn \l__integramme_categories_seq
              {
                \prop_get:NnN \l__integramme_item_count_prop {##1} \l_tmpb_tl
                \int_compare:nNnT { \l_tmpb_tl } = {0}
                  {
                    \msg_error:nnn { integramme } { empty-category } {##1}
                    \bool_set_false:N \l__integramme_valid_bool
                  }
                \int_compare:nNnF
                  { \l_tmpb_tl }
                  =
                  { \l__integramme_order_int }
                  {
                    \msg_error:nnn { integramme } { unequal-sizes } {##1}
                    \bool_set_false:N \l__integramme_valid_bool
                  }
              }
          }
      }

    \bool_if:NT \l__integramme_valid_bool
      {
        \tl_if_eq:NnT \l__integramme_mode_tl { solution }
          {
            \seq_if_empty:NTF \l__integramme_solution_input_seq
              {
                \msg_error:nn { integramme } { missing-solution }
                \bool_set_false:N \l__integramme_valid_bool
              }
              {
                \__integramme_normalize_solution_inputs:
                \int_compare:nNnF
                  { \seq_count:N \l__integramme_solution_seq }
                  =
                  { \l__integramme_order_int }
                  {
                    \msg_error:nnx { integramme } { bad-solution-size }
                      { \int_use:N \l__integramme_order_int }
                    \bool_set_false:N \l__integramme_valid_bool
                  }
                \prop_clear:N \l__integramme_solution_seen_prop
                \seq_map_inline:Nn \l__integramme_solution_seq
                  { \__integramme_validate_solution_row:n {##1} }
              }
          }
      }

    \bool_if:NT \l__integramme_valid_bool
      { \__integramme_resolve_manual_marks: }
  }

% --------------------------------------------------------------------------
% Helpers
% --------------------------------------------------------------------------
\cs_new:Npn \__integramme_cat_id:n #1
  { \seq_item:Nn \l__integramme_categories_seq {#1} }

\cs_new:Npn \__integramme_item_id:nn #1#2
  {
    \prop_item:Ne \l__integramme_item_id_prop
      {
        \__integramme_index_key:nn
          { \__integramme_cat_id:n {#1} }
          {#2}
      }
  }

\cs_new:Npn \__integramme_item:nn #1#2
  {
    \prop_item:Ne \l__integramme_item_label_prop
      {
        \__integramme_index_key:nn
          { \__integramme_cat_id:n {#1} }
          {#2}
      }
  }

\cs_new:Npn \__integramme_dim_units:n #1
  {
    \fp_eval:n
      { \dim_to_fp:n {#1} / \dim_to_fp:n { \l__integramme_cell_dim } }
  }

\prg_new_conditional:Npnn \__integramme_solution_match:nnnn #1#2#3#4 { T, F, TF }
  {
    \bool_set_false:N \l_tmpa_bool
    \seq_map_inline:Nn \l__integramme_solution_seq
      {
        \clist_set:Nn \l_tmpa_clist {##1}
        \tl_set:Nx \l_tmpa_tl { \clist_item:Nn \l_tmpa_clist {#1} }
        \tl_set:Nx \l_tmpb_tl { \clist_item:Nn \l_tmpa_clist {#3} }
        \tl_set:Nx \l_tmpc_tl { \__integramme_item_id:nn {#1}{#2} }
        \tl_set:Nx \l_tmpd_tl { \__integramme_item_id:nn {#3}{#4} }
        \tl_trim_spaces:N \l_tmpa_tl
        \tl_trim_spaces:N \l_tmpb_tl
        \tl_trim_spaces:N \l_tmpc_tl
        \tl_trim_spaces:N \l_tmpd_tl
        \tl_if_eq:NNT \l_tmpa_tl \l_tmpc_tl
          {
            \tl_if_eq:NNT \l_tmpb_tl \l_tmpd_tl
              {
                \bool_set_true:N \l_tmpa_bool
                \seq_map_break:
              }
          }
      }
    \bool_if:NTF \l_tmpa_bool { \prg_return_true: } { \prg_return_false: }
  }

% --------------------------------------------------------------------------
% Automatic layout measurement
% --------------------------------------------------------------------------
\cs_new_protected:Npn \__integramme_measure_layout:
  {
    % ----- Width of the row-label column (automatic when side-label-width=0pt)
    \dim_compare:nNnT { \l__integramme_side_label_dim } = { 0pt }
      {
        \dim_zero:N \l_tmpa_dim
        \int_step_inline:nn { \l__integramme_category_count_int - 1 }
          {
            \int_step_inline:nn { \l__integramme_order_int }
              {
                \hbox_set:Nn \l_tmpa_box
                  { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} }
                \dim_compare:nNnT { \box_wd:N \l_tmpa_box } > { \l_tmpa_dim }
                  { \dim_set:Nn \l_tmpa_dim { \box_wd:N \l_tmpa_box } }
              }
          }
        \dim_set:Nn \l__integramme_side_label_dim
          { \l_tmpa_dim + 2\l__integramme_side_padding_dim }
        \dim_compare:nNnT
          { \l__integramme_side_label_dim }
          <
          { \l__integramme_side_label_min_dim }
          { \dim_set_eq:NN \l__integramme_side_label_dim \l__integramme_side_label_min_dim }
      }

    % ----- Decide whether all top labels must be slanted.
    % In auto mode, one label that does not fit its cell is enough to slant
    % the complete top header. This keeps the visual rhythm consistent.
    \tl_if_eq:NnTF \l__integramme_column_labels_tl { slanted }
      { \bool_set_true:N \l__integramme_top_slanted_bool }
      {
        \tl_if_eq:NnTF \l__integramme_column_labels_tl { horizontal }
          { \bool_set_false:N \l__integramme_top_slanted_bool }
          {
            \bool_set_false:N \l__integramme_top_slanted_bool
            \dim_set:Nn \l_tmpa_dim
              { \l__integramme_cell_dim - 2\l__integramme_top_padding_dim }
            \int_step_inline:nnnn {2}{1}{\l__integramme_category_count_int}
              {
                \int_step_inline:nn { \l__integramme_order_int }
                  {
                    \hbox_set:Nn \l_tmpa_box
                      { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} }
                    \dim_compare:nNnT { \box_wd:N \l_tmpa_box } > { \l_tmpa_dim }
                      { \bool_set_true:N \l__integramme_top_slanted_bool }
                  }
              }
          }
      }

    % ----- Effective height of the short vertical stem used by slanted
    % separators.  By default it is approximately one capital-letter height.
    \dim_compare:nNnTF { \l__integramme_guide_stem_dim } = { 0pt }
      {
        \hbox_set:Nn \l_tmpa_box { {\l__integramme_font_tl X} }
        \dim_set:Nn \l__integramme_guide_stem_effective_dim
          { \box_ht:N \l_tmpa_box + \box_dp:N \l_tmpa_box }
      }
      {
        \dim_set_eq:NN
          \l__integramme_guide_stem_effective_dim
          \l__integramme_guide_stem_dim
      }

    % ----- Required top-label height
    \dim_set_eq:NN \l__integramme_top_label_dim \l__integramme_top_label_min_dim
    \bool_if:NTF \l__integramme_top_slanted_bool
      {
        \int_step_inline:nnnn {2}{1}{\l__integramme_category_count_int}
          {
            \int_step_inline:nn { \l__integramme_order_int }
              {
                \hbox_set:Nn \l_tmpa_box
                  { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} }
                \fp_set:Nn \l_tmpa_fp
                  {
                    \dim_to_fp:n { \box_wd:N \l_tmpa_box }
                    * sind(\l__integramme_top_angle_fp)
                    +
                    \dim_to_fp:n { \box_ht:N \l_tmpa_box + \box_dp:N \l_tmpa_box }
                    * cosd(\l__integramme_top_angle_fp)
                  }
                \dim_set:Nn \l_tmpb_dim
                  { \fp_to_dim:n { \l_tmpa_fp } + 2\l__integramme_top_padding_dim }
                \dim_compare:nNnT
                  { \l_tmpb_dim }
                  >
                  { \l__integramme_top_label_dim }
                  { \dim_set_eq:NN \l__integramme_top_label_dim \l_tmpb_dim }
              }
          }

        % The bent guide itself must also fit inside the label area.
        \fp_set:Nn \l_tmpa_fp
          {
            \dim_to_fp:n { \l__integramme_guide_stem_effective_dim }
            + \l__integramme_guide_length_fp
              * \dim_to_fp:n { \l__integramme_cell_dim }
              * sind(\l__integramme_top_angle_fp)
          }
        \dim_set:Nn \l_tmpb_dim
          { \fp_to_dim:n { \l_tmpa_fp } + \l__integramme_top_padding_dim }
        \dim_compare:nNnT
          { \l_tmpb_dim }
          >
          { \l__integramme_top_label_dim }
          { \dim_set_eq:NN \l__integramme_top_label_dim \l_tmpb_dim }
      }
      {
        \dim_zero:N \l_tmpa_dim
        \int_step_inline:nnnn {2}{1}{\l__integramme_category_count_int}
          {
            \int_step_inline:nn { \l__integramme_order_int }
              {
                \hbox_set:Nn \l_tmpa_box
                  { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} }
                \dim_set:Nn \l_tmpb_dim { \box_ht:N \l_tmpa_box + \box_dp:N \l_tmpa_box }
                \dim_compare:nNnT { \l_tmpb_dim } > { \l_tmpa_dim }
                  { \dim_set_eq:NN \l_tmpa_dim \l_tmpb_dim }
              }
          }
        \dim_set:Nn \l_tmpb_dim { \l_tmpa_dim + 2\l__integramme_top_padding_dim }
        \dim_compare:nNnT { \l_tmpb_dim } > { \l__integramme_top_label_dim }
          { \dim_set_eq:NN \l__integramme_top_label_dim \l_tmpb_dim }
      }
  }

% --------------------------------------------------------------------------
% Drawing helpers
% --------------------------------------------------------------------------
\cs_new_protected:Npn \__integramme_draw_mark:nnn #1#2#3
  {
    \node[
      font=\l__integramme_mark_font_tl,
      text=\l__integramme_text_color_tl
    ] at (#2,#3)
      {
        \str_if_eq:nnTF {#1} {yes}
          { \l__integramme_yes_tl }
          { \l__integramme_no_tl }
      };
  }

\cs_new_protected:Npn \__integramme_draw_pair_block:nn #1#2
  {
    % #1 = row category index i
    % #2 = column category index j (j > i)
    \fp_set:Nn \l_tmpa_fp
      { ( \l__integramme_category_count_int - #2 ) * \l__integramme_order_int }
    \fp_set:Nn \l_tmpb_fp
      { - ( #1 - 1 ) * \l__integramme_order_int }

    % Thin inner grid.
    \int_step_inline:nn { \l__integramme_order_int - 1 }
      {
        \draw[
          draw=\l__integramme_grid_color_tl,
          line~width=\dim_use:N \l__integramme_rule_dim
        ]
          ({\fp_use:N\l_tmpa_fp + ##1},{\fp_use:N\l_tmpb_fp}) --
          ({\fp_use:N\l_tmpa_fp + ##1},{\fp_use:N\l_tmpb_fp - \l__integramme_order_int});
        \draw[
          draw=\l__integramme_grid_color_tl,
          line~width=\dim_use:N \l__integramme_rule_dim
        ]
          ({\fp_use:N\l_tmpa_fp},{\fp_use:N\l_tmpb_fp - ##1}) --
          ({\fp_use:N\l_tmpa_fp + \l__integramme_order_int},{\fp_use:N\l_tmpb_fp - ##1});
      }

    % Thick block border.
    \draw[
      draw=\l__integramme_grid_color_tl,
      line~width=\dim_use:N \l__integramme_block_rule_dim
    ]
      ({\fp_use:N\l_tmpa_fp},{\fp_use:N\l_tmpb_fp}) rectangle
      ({\fp_use:N\l_tmpa_fp + \l__integramme_order_int},
       {\fp_use:N\l_tmpb_fp - \l__integramme_order_int});

    % Optional marks.
    \tl_if_eq:NnTF \l__integramme_mode_tl { solution }
      {
        \int_step_inline:nn { \l__integramme_order_int }
          {
            \int_set:Nn \l__integramme_draw_row_int {##1}
            \int_step_inline:nn { \l__integramme_order_int }
              {
                \__integramme_solution_match:nnnnTF
                  {#1}{\l__integramme_draw_row_int}{#2}{####1}
                  {
                    \__integramme_draw_mark:nnn {yes}
                      {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }}
                      {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }}
                  }
                  {
                    \__integramme_draw_mark:nnn {no}
                      {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }}
                      {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }}
                  }
              }
          }
      }
      {
        \tl_if_eq:NnT \l__integramme_mode_tl { manual }
          {
            \int_step_inline:nn { \l__integramme_order_int }
              {
                \int_set:Nn \l__integramme_draw_row_int {##1}
                \int_step_inline:nn { \l__integramme_order_int }
                  {
                    \tl_set:Nx \l_tmpa_tl
                      {
                        \__integramme_mark_key:nnnn
                          {#1}{\l__integramme_draw_row_int}{#2}{####1}
                      }
                    \prop_get:NVNT \l__integramme_manual_marks_prop \l_tmpa_tl \l_tmpb_tl
                      {
                        \tl_if_eq:NnTF \l_tmpb_tl {yes}
                          {
                            \__integramme_draw_mark:nnn {yes}
                              {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }}
                              {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }}
                          }
                          {
                            \__integramme_draw_mark:nnn {no}
                              {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }}
                              {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }}
                          }
                      }
                  }
              }
          }
      }
  }

\cs_new_protected:Npn \__integramme_draw_side_labels:
  {
    \fp_set:Nn \l_tmpc_fp { \__integramme_dim_units:n { \l__integramme_side_label_dim } }
    \fp_set:Nn \l_tmpd_fp { \__integramme_dim_units:n { \l__integramme_side_title_dim } }
    \fp_set:Nn \l_tmpe_fp { \__integramme_dim_units:n { \l__integramme_side_padding_dim } }

    \int_step_inline:nn { \l__integramme_category_count_int - 1 }
      {
        \fp_set:Nn \l_tmpa_fp { - (##1 - 1) * \l__integramme_order_int }
        \tl_set:Nx \l_tmpa_tl { \__integramme_cat_id:n {##1} }
        \prop_get:NVN \l__integramme_titles_prop \l_tmpa_tl \l_tmpb_tl

        % Outer side block: thick border.
        \draw[
          draw=\l__integramme_grid_color_tl,
          line~width=\dim_use:N \l__integramme_block_rule_dim
        ]
          ({-\fp_use:N\l_tmpc_fp-\fp_use:N\l_tmpd_fp},{\fp_use:N\l_tmpa_fp}) rectangle
          (0,{\fp_use:N\l_tmpa_fp-\l__integramme_order_int});

        % Thin divider between the vertical category title and row labels.
        \draw[
          draw=\l__integramme_grid_color_tl,
          line~width=\dim_use:N \l__integramme_rule_dim
        ]
          ({-\fp_use:N\l_tmpc_fp},{\fp_use:N\l_tmpa_fp}) --
          ({-\fp_use:N\l_tmpc_fp},{\fp_use:N\l_tmpa_fp-\l__integramme_order_int});

        % Thin row separators only through the item-label area.
        \int_step_inline:nn { \l__integramme_order_int - 1 }
          {
            \draw[
              draw=\l__integramme_grid_color_tl,
              line~width=\dim_use:N \l__integramme_rule_dim
            ]
              ({-\fp_use:N\l_tmpc_fp},{\fp_use:N\l_tmpa_fp-####1}) --
              (0,{\fp_use:N\l_tmpa_fp-####1});
          }

        % Category title (vertical).
        \node[
          font=\l__integramme_title_font_tl,
          text=\l__integramme_text_color_tl,
          rotate=90,
          anchor=center
        ]
          at
          ({-\fp_use:N\l_tmpc_fp-.5*\fp_use:N\l_tmpd_fp},
           {\fp_use:N\l_tmpa_fp-.5*\l__integramme_order_int})
          {\l_tmpb_tl};

        % Row item labels.
        \int_step_inline:nn { \l__integramme_order_int }
          {
            \node[
              font=\l__integramme_font_tl,
              text=\l__integramme_text_color_tl,
              anchor=west,
              inner~sep=0pt
            ]
              at
              ({-\fp_use:N\l_tmpc_fp+\fp_use:N\l_tmpe_fp},
               {\fp_use:N\l_tmpa_fp-####1+.5})
              {\__integramme_item:nn {##1}{####1}};
          }
      }
  }

\cs_new_protected:Npn \__integramme_draw_top_labels:
  {
    \fp_set:Nn \l_tmpc_fp { \__integramme_dim_units:n { \l__integramme_top_label_dim } }
    \fp_set:Nn \l_tmpd_fp { \__integramme_dim_units:n { \l__integramme_title_height_dim } }
    \fp_set:Nn \l_tmpe_fp { \__integramme_dim_units:n { \l__integramme_top_padding_dim } }
    \fp_set:Nn \l_tmph_fp
      { \__integramme_dim_units:n { \l__integramme_guide_stem_effective_dim } }

    % Categories are reversed on top: k, k-1, ..., 2.
    \int_step_inline:nnnn {\l__integramme_category_count_int}{-1}{2}
      {
        \fp_set:Nn \l_tmpa_fp
          { ( \l__integramme_category_count_int - ##1 ) * \l__integramme_order_int }
        \tl_set:Nx \l_tmpa_tl { \__integramme_cat_id:n {##1} }
        \prop_get:NVN \l__integramme_titles_prop \l_tmpa_tl \l_tmpb_tl

        % Complete header block with thick outline.
        \draw[
          draw=\l__integramme_grid_color_tl,
          line~width=\dim_use:N \l__integramme_block_rule_dim
        ]
          ({\fp_use:N\l_tmpa_fp},0) rectangle
          ({\fp_use:N\l_tmpa_fp+\l__integramme_order_int},
           {\fp_use:N\l_tmpc_fp+\fp_use:N\l_tmpd_fp});

        % Thin separator below category title.
        \draw[
          draw=\l__integramme_grid_color_tl,
          line~width=\dim_use:N \l__integramme_rule_dim
        ]
          ({\fp_use:N\l_tmpa_fp},{\fp_use:N\l_tmpc_fp}) --
          ({\fp_use:N\l_tmpa_fp+\l__integramme_order_int},{\fp_use:N\l_tmpc_fp});

        % Category title.
        \node[
          font=\l__integramme_title_font_tl,
          text=\l__integramme_text_color_tl,
          align=center,
          text~width=\dim_eval:n
            { \l__integramme_order_int\l__integramme_cell_dim - 2\l__integramme_top_padding_dim },
          inner~sep=0pt
        ]
          at
          ({\fp_use:N\l_tmpa_fp+.5*\l__integramme_order_int},
           {\fp_use:N\l_tmpc_fp+.5*\fp_use:N\l_tmpd_fp})
          {\l_tmpb_tl};

        \bool_if:NTF \l__integramme_top_slanted_bool
          {
            % Diagonal guides, parallel to the text.
            \bool_if:NT \l__integramme_guides_bool
              {
                \int_step_inline:nn { \l__integramme_order_int }
                  {
                    \fp_set:Nn \l_tmpf_fp
                      { \l__integramme_guide_length_fp * cosd(\l__integramme_top_angle_fp) }
                    \fp_set:Nn \l_tmpg_fp
                      { \l__integramme_guide_length_fp * sind(\l__integramme_top_angle_fp) }
                    \draw[
                      draw=\l__integramme_grid_color_tl,
                      line~width=\dim_use:N \l__integramme_rule_dim,
                      opacity=\fp_use:N \l__integramme_guide_opacity_fp
                    ]
                      ({\fp_use:N\l_tmpa_fp+####1-1},0) --
                      ({\fp_use:N\l_tmpa_fp+####1-1},{\fp_use:N\l_tmph_fp}) --
                      ({\fp_use:N\l_tmpa_fp+####1-1+\fp_use:N\l_tmpf_fp},
                       {\fp_use:N\l_tmph_fp+\fp_use:N\l_tmpg_fp});
                  }
              }

            % Slanted labels are shifted half a cell right, so each label sits
            % midway between two consecutive guide/column lines.
            \int_step_inline:nn { \l__integramme_order_int }
              {
                \node[
                  font=\l__integramme_font_tl,
                  text=\l__integramme_text_color_tl,
                  rotate=\fp_use:N \l__integramme_top_angle_fp,
                  anchor=south~west,
                  inner~sep=0pt
                ]
                  at
                  ({\fp_use:N\l_tmpa_fp+####1-.5},
                   {\fp_use:N\l_tmpe_fp})
                  {\__integramme_item:nn {##1}{####1}};
              }
          }
          {
            % Horizontal mode: ordinary vertical separators.
            \int_step_inline:nn { \l__integramme_order_int - 1 }
              {
                \draw[
                  draw=\l__integramme_grid_color_tl,
                  line~width=\dim_use:N \l__integramme_rule_dim
                ]
                  ({\fp_use:N\l_tmpa_fp+####1},0) --
                  ({\fp_use:N\l_tmpa_fp+####1},{\fp_use:N\l_tmpc_fp});
              }

            \int_step_inline:nn { \l__integramme_order_int }
              {
                \node[
                  font=\l__integramme_font_tl,
                  text=\l__integramme_text_color_tl,
                  anchor=center,
                  inner~sep=0pt
                ]
                  at
                  ({\fp_use:N\l_tmpa_fp+####1-.5},{.5*\fp_use:N\l_tmpc_fp})
                  {\__integramme_item:nn {##1}{####1}};
              }
          }
      }
  }

\cs_new_protected:Npn \__integramme_prepare_total_width:
  {
    \int_set:Nn \l_tmpa_int
      {
        ( \l__integramme_category_count_int - 1 )
        * \l__integramme_order_int
      }
    \dim_set:Nn \l__integramme_total_width_dim
      {
        \l__integramme_side_title_dim
        + \l__integramme_side_label_dim
        + \l_tmpa_int \l__integramme_cell_dim
      }
    \bool_if:NT \l__integramme_overflow_warning_bool
      {
        \dim_compare:nNnT { \l__integramme_total_width_dim } > { \linewidth }
          {
            \msg_warning:nnxx { integramme } { grid-too-wide }
              { \dim_use:N \l__integramme_total_width_dim }
              { \dim_use:N \linewidth }
          }
      }
  }

\cs_new_protected:Npn \__integramme_print_indices:
  {
    \tl_if_empty:NF \l__integramme_indices_tl
      {
        \par\vspace{\l__integramme_indices_sep_dim}
        \begin{minipage}{\dim_use:N \l__integramme_total_width_dim}
          \raggedright
          {\l__integramme_indices_font_tl
            \begin{enumerate}
              \setlength{\topsep}{0pt}
              \setlength{\partopsep}{0pt}
              \setlength{\parsep}{0pt}
              \setlength{\itemsep}{0.2\baselineskip}
              \l__integramme_indices_tl
            \end{enumerate}
          }
        \end{minipage}
      }
  }

\cs_new_protected:Npn \__integramme_draw:
  {
    \__integramme_measure_layout:
    \__integramme_prepare_total_width:
    \begin{tikzpicture}[
      x=\dim_use:N \l__integramme_cell_dim,
      y=\dim_use:N \l__integramme_cell_dim,
      baseline=(current~bounding~box.north)
    ]
      % Pairwise blocks: staircase layout.
      \int_step_inline:nn { \l__integramme_category_count_int - 1 }
        {
          \int_step_inline:nnnn
            {\l__integramme_category_count_int}{-1}{##1+1}
            { \__integramme_draw_pair_block:nn {##1}{####1} }
        }
      \__integramme_draw_side_labels:
      \__integramme_draw_top_labels:
    \end{tikzpicture}
    \__integramme_print_indices:
  }

% --------------------------------------------------------------------------
% Environment
% --------------------------------------------------------------------------
\NewDocumentEnvironment { integramme } { O{} }
  {
    \group_begin:
    \bool_set_true:N \l__integramme_valid_bool
    \seq_clear:N  \l__integramme_categories_seq
    \prop_clear:N \l__integramme_titles_prop
    \prop_clear:N \l__integramme_item_count_prop
    \prop_clear:N \l__integramme_item_id_prop
    \prop_clear:N \l__integramme_item_label_prop
    \prop_clear:N \l__integramme_item_index_prop
    \seq_clear:N  \l__integramme_solution_seq
    \seq_clear:N  \l__integramme_solution_input_seq
    \seq_clear:N  \l__integramme_manual_yes_seq
    \seq_clear:N  \l__integramme_manual_no_seq
    \prop_clear:N \l__integramme_manual_marks_prop
    \tl_clear:N   \l__integramme_indices_tl
    \keys_set:nn { integramme } {#1}
    \__integramme_apply_preset:
  }
  {
    \__integramme_validate:
    \bool_if:NT \l__integramme_valid_bool { \__integramme_draw: }
    \group_end:
  }

\ExplSyntaxOff
%</package>
% \end{macrocode}
%
% \Finale
