Author SHA1 Message Date
kua-agentandClaude Opus 5 39130f41e6 feat: one merged MMD band, Scan in the main bar, shorter icon row, signed build
Ben, 2026-08-28: four changes, then build it, sign it, and make it deployable
through Intune without alerts.

1. The row of icons is a fifth shorter. Every toolbar label mode comes down
   together so the proportions between them are unchanged: 34 -> 27 for a
   caption beside the icon, 52/56 -> 42/45 for a caption under it, with the
   padding reduced to match. The row measures 48px on screen where it was 60.

2. Scan sits in the main bar. It is a solid white button on the band, to the
   left of the window buttons, so a person can pull pages off a copier whatever
   the toolbar is showing. Behind it, Services/ScanService.cs talks to the
   copier through WIA - the imaging service that ships with Windows, reached
   late-bound so a machine without it fails at a check instead of at load - and
   Features/Scan/ScanDialog picks the copier, the feed, the colour, the detail
   and the paper. Pages come back as images and take the same route into a
   document as an imported photograph. "Make the words searchable" runs the
   text recognition the application already carries.

3. The official MMD lockup, in white, because the band is red now.

4. The MMD design system locked on 2026-08-28: ONE merged top bar carrying the
   mark, the name, the tabs and the tools; 4px corners; 24px window buttons;
   the bevel on title bars only. Colours are NOT adjusted - the Red Plate
   silver and ink set already in Themes/MMD.xaml is untouched.

Also fixed, both found by looking at the running application at 800x600:

  - The default 1000x700 window does not fit an 800x600 plant screen, and a
    window taller than the screen puts its own title bar - and therefore Scan,
    the window buttons and the tabs - above the top edge where nobody can reach
    them. MainWindow now starts maximised when the screen cannot hold the
    default.
  - The close button was invisible: CaptionCloseBrush defaults to the danger
    red, which on a red band is red on red. The window buttons are white now,
    and close inverts to a white plate with a red cross on hover.

Built and run on mmd-win-test-01 at 800x600 through keyboard and mouse only.
Signed with the internal MMD code-signing certificate and timestamped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 03:25:42 +00:00
kua-agentandClaude Opus 5 e02d219736 docs: product documentation, design working files, and the failed design review
Adds the required doc set under docs/mmd-pdf/ - PRD, feature backlog with
status and sign-off per feature, screens with wireframes, test cases with the
test data and the agent-tasking prompts, and the sign-off log.

The sign-off log records the independent design review of 2026-08-27:
DESIGN-FAIL, three blockers and five major findings, which is why every design
feature in the backlog is blocked and the logo/token branch is unmerged.

Design working files live in docs/mmd-pdf/design/ so the next agent can
regenerate and re-render them. redplate.css there is the MMD shell design
system lifted verbatim from the session that owns it - do not edit it.

Also carries the session handover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:25:29 +02:00
kua-agentandClaude Opus 5 bfb9845c46 design: official MMD lockup in the title bar, and shell-system colour values
Replaces the app icon and word-mark drawn by an agent with the official MMD
artwork (mmd-logo-red.svg / mmd-logo.svg, unmodified). The Windows icon is the
lockup's baobab disc, cropped from the same file - the only square element in
it. It reads cleanly from 32px and goes muddy at 16-24px; no official small
mark exists, so that is an open question, not a solved one.

Themes/MMD.xaml carries the values of the new MMD shell design system
("red plate"): F1F1EC chrome, E4E3DC rails, F3F3F0 around the page, D4D0C6
desk, 23221E ink with 3C3A34 / 767368 / 6B6A64 below it, C8102E red with
A80D24 hover and 8E0B20 pressed, borders E6E5DF / D7D6CE / C7C6BD / B9B8AE.

NOT ACCEPTED. The design these values came from failed an independent design
review on 2026-08-27 with 3 blockers and 5 major findings. This branch is an
implementation preview only and must not be merged until the design is signed
off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:20:04 +02:00
63 changed files with 3614 additions and 52 deletions
+163
View File
@@ -0,0 +1,163 @@
<!-- Pull pages off a copier. Chrome follows the MMD design system locked on 2026-08-28:
a front-window title bar (white to silver, 3px red top edge, ink text), 4px corners,
flat buttons, one red fill for the single action the person came to do. Every colour and
radius is a theme key from Themes/MMD.xaml - nothing is picked here. -->
<Window x:Class="MmdPdf.Features.Scan.ScanDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Scan a document"
Width="520" SizeToContent="Height"
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
Background="Transparent"
TextOptions.TextFormattingMode="Display"
TextOptions.TextRenderingMode="ClearType"
UseLayoutRounding="True" SnapsToDevicePixels="True"
WindowStartupLocation="CenterOwner">
<Border Background="{DynamicResource BgFlyout}"
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1"
CornerRadius="{DynamicResource ControlCornerRadius}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title bar: the front-window treatment from the design system -->
<Border Grid.Row="0" Height="34"
Background="{DynamicResource WindowFrontTitleBrush}"
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="0,3,0,0"
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
<Grid Margin="12,0,8,0">
<TextBlock x:Name="TitleText" Text="Scan a document" VerticalAlignment="Center"
FontFamily="Segoe UI" FontSize="12.5" FontWeight="Bold"
Foreground="{DynamicResource TextBrush}"/>
<Button x:Name="CloseChrome" Click="Cancel_Click" HorizontalAlignment="Right"
VerticalAlignment="Center" Width="24" Height="24"
Background="Transparent" BorderThickness="0" Cursor="Hand"
Foreground="{DynamicResource TextBrush}" Content="&#x2715;"
FontFamily="Segoe UI" FontSize="11"/>
</Grid>
</Border>
<!-- Body -->
<Grid Grid.Row="1" Margin="16,14,16,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Column="0">
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="COPIER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
Foreground="{DynamicResource DimTextBrush}"/>
<ComboBox x:Name="CopierBox" Grid.Column="1" Height="26" FontSize="11.5"/>
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="FEED" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
Foreground="{DynamicResource DimTextBrush}"/>
<ComboBox x:Name="FeedBox" Grid.Column="1" Height="26" FontSize="11.5"/>
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="COLOUR" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
Foreground="{DynamicResource DimTextBrush}"/>
<ComboBox x:Name="ColourBox" Grid.Column="1" Height="26" FontSize="11.5"/>
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="DETAIL" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
Foreground="{DynamicResource DimTextBrush}"/>
<ComboBox x:Name="DetailBox" Grid.Column="1" Height="26" FontSize="11.5"/>
</Grid>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="PAPER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
Foreground="{DynamicResource DimTextBrush}"/>
<ComboBox x:Name="PaperBox" Grid.Column="1" Height="26" FontSize="11.5"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="AFTERWARDS" VerticalAlignment="Top" Margin="0,2,0,0"
FontFamily="Consolas" FontSize="9.5"
Foreground="{DynamicResource DimTextBrush}"/>
<StackPanel Grid.Column="1">
<CheckBox x:Name="SearchableBox" IsChecked="True" FontSize="11.5">
<TextBlock Text="Make the words searchable, in French and English"
TextWrapping="Wrap" MaxWidth="180"/>
</CheckBox>
<CheckBox x:Name="StraightenBox" Margin="0,6,0,0" FontSize="11.5">
<TextBlock Text="Straighten crooked pages" TextWrapping="Wrap" MaxWidth="180"/>
</CheckBox>
</StackPanel>
</Grid>
</StackPanel>
<!-- Sheet preview: what one page will look like -->
<Border Grid.Column="1" Width="132" Margin="16,0,0,0"
Background="{DynamicResource BgCanvas}"
BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1"
CornerRadius="{DynamicResource ControlCornerRadius}">
<Border Width="96" Height="132" Margin="10" VerticalAlignment="Center"
Background="White" BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1">
<StackPanel Margin="9,9,8,0">
<Rectangle Height="3" Width="56" HorizontalAlignment="Left" Margin="0,0,0,5"
Fill="{DynamicResource DimTextBrush}"/>
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
<Rectangle Height="2" Width="66" HorizontalAlignment="Left" Margin="0,0,0,4"
Fill="{DynamicResource TreeLineBrush}"/>
<Rectangle Height="2" Width="44" HorizontalAlignment="Left" Margin="0,0,0,4"
Fill="{DynamicResource TreeLineBrush}"/>
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
<Rectangle Height="2" Width="60" HorizontalAlignment="Left" Fill="{DynamicResource TreeLineBrush}"/>
</StackPanel>
</Border>
</Border>
</Grid>
<!-- Footer -->
<Border Grid.Row="2" Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource PaneBorderBrush}" BorderThickness="0,1,0,0"
CornerRadius="0,0,4,4" Padding="16,10">
<Grid>
<TextBlock x:Name="FootNote" VerticalAlignment="Center" FontSize="10.5"
Foreground="{DynamicResource DimTextBrush}"
Text="Pages arrive as one document you can edit"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="CancelBtn" Content="Cancel" Click="Cancel_Click"
Height="28" Padding="13,0" Margin="0,0,8,0" FontSize="11.5" FontWeight="SemiBold"/>
<Button x:Name="ScanBtn" Content="Scan" Click="Scan_Click"
Height="28" Padding="13,0" FontSize="11.5" FontWeight="SemiBold"
Background="{DynamicResource PrimaryBrush}"
Foreground="{DynamicResource OnPrimaryBrush}"
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="1"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</Window>
+186
View File
@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using MmdPdf.Services;
namespace MmdPdf.Features.Scan
{
/// <summary>
/// Asks the copier for pages. The person picks the machine, the feed, how much ink and how
/// much detail; everything else is the copier's own default. The pages come back as image
/// files which MainWindow turns into one document.
///
/// A run cannot be interrupted mid-sheet - that is the copier's hardware, not a decision made
/// here - so Cancel stops after the sheet in progress and keeps the pages already read.
/// </summary>
public partial class ScanDialog : Window
{
private readonly List<ScannerInfo> _scanners;
private CancellationTokenSource? _cancel;
private bool _running;
/// <summary>Image files, one per page, in the order the copier read them.</summary>
internal string[] Pages { get; private set; } = Array.Empty<string>();
/// <summary>The person asked for the words to be made searchable afterwards.</summary>
internal bool MakeSearchable { get; private set; }
/// <summary>The person asked for crooked pages to be straightened afterwards.</summary>
internal bool Straighten { get; private set; }
public ScanDialog()
{
InitializeComponent();
_scanners = ScanService.IsAvailable ? ScanService.ListScanners() : new List<ScannerInfo>();
foreach (var s in _scanners) CopierBox.Items.Add(s.Name);
if (_scanners.Count > 0)
{
CopierBox.SelectedIndex = 0;
}
else
{
CopierBox.Items.Add(ScanService.IsAvailable
? "No copier found on this machine"
: "This machine has no imaging service");
CopierBox.SelectedIndex = 0;
CopierBox.IsEnabled = false;
ScanBtn.IsEnabled = false;
FootNote.Text = "Switch the copier on, or connect it, then open this again";
}
FeedBox.Items.Add("Feeder, one side");
FeedBox.Items.Add("Feeder, both sides");
FeedBox.Items.Add("Glass, one sheet");
FeedBox.SelectedIndex = 0;
ColourBox.Items.Add("Black and white");
ColourBox.Items.Add("Shades of grey");
ColourBox.Items.Add("Colour");
ColourBox.SelectedIndex = 0;
DetailBox.Items.Add("200 dpi, quick");
DetailBox.Items.Add("300 dpi, good for text");
DetailBox.Items.Add("600 dpi, fine detail");
DetailBox.SelectedIndex = 1;
foreach (var p in ScanService.PaperSizes) PaperBox.Items.Add(p);
PaperBox.SelectedIndex = 0;
}
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed) DragMove();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
if (_running)
{
_cancel?.Cancel();
FootNote.Text = "Stopping after this sheet…";
return;
}
DialogResult = false;
Close();
}
private async void Scan_Click(object sender, RoutedEventArgs e)
{
if (_running || _scanners.Count == 0) return;
var device = _scanners[Math.Max(0, CopierBox.SelectedIndex)];
var feed = FeedBox.SelectedIndex switch
{
1 => ScanFeed.FeederBothSides,
2 => ScanFeed.Flatbed,
_ => ScanFeed.FeederOneSide,
};
var colour = ColourBox.SelectedIndex switch
{
1 => ScanColour.Greyscale,
2 => ScanColour.Colour,
_ => ScanColour.BlackAndWhite,
};
int dpi = DetailBox.SelectedIndex switch
{
0 => 200,
2 => 600,
_ => 300,
};
string paper = PaperBox.SelectedItem as string ?? "A4 portrait";
MakeSearchable = SearchableBox.IsChecked == true;
Straighten = StraightenBox.IsChecked == true;
_running = true;
_cancel = new CancellationTokenSource();
ScanBtn.IsEnabled = false;
CancelBtn.Content = "Stop";
SetInputsEnabled(false);
FootNote.Text = feed == ScanFeed.Flatbed ? "Reading the sheet on the glass…" : "Reading…";
var progress = new Progress<int>(n =>
FootNote.Text = n == 1 ? "1 page read" : n.ToString(CultureInfo.InvariantCulture) + " pages read");
try
{
var token = _cancel.Token;
var reporter = (IProgress<int>)progress;
string[] pages = await Task.Run(() =>
ScanService.Acquire(device.DeviceId, feed, colour, dpi, paper,
maxPages: feed == ScanFeed.Flatbed ? 1 : 200,
cancel: token,
onPage: reporter.Report).ToArray(), token);
Pages = pages;
if (pages.Length == 0)
{
FootNote.Text = "The copier returned no pages";
ResetAfterRun();
return;
}
DialogResult = true;
Close();
}
catch (OperationCanceledException)
{
FootNote.Text = "Stopped";
ResetAfterRun();
}
catch (Exception ex)
{
FootNote.Text = "The copier could not be used: " + ex.Message;
ResetAfterRun();
}
}
private void ResetAfterRun()
{
_running = false;
_cancel?.Dispose();
_cancel = null;
ScanBtn.IsEnabled = _scanners.Count > 0;
CancelBtn.Content = "Cancel";
SetInputsEnabled(true);
}
private void SetInputsEnabled(bool on)
{
CopierBox.IsEnabled = on && _scanners.Count > 0;
FeedBox.IsEnabled = on;
ColourBox.IsEnabled = on;
DetailBox.IsEnabled = on;
PaperBox.IsEnabled = on;
SearchableBox.IsEnabled = on;
StraightenBox.IsEnabled = on;
}
}
}
+169
View File
@@ -0,0 +1,169 @@
# HANDOVER — MMD PDF (Windows PDF editor for staff)
**Written** 2026-08-27 15:18:53 +04 (11:18 UTC)
**Session** mmd-projects-87 · **Repo** `gitea.baobab-ts.com/mmd-it/mmd-pdf`
---
## What this is
MMD staff had no sanctioned way to write inside PDF documents. PDFgear was
blocked on 2026-07-20 for forging the Windows default-PDF registry setting.
This session picked a replacement, proved it, forked it, and started turning it
into an MMD product.
**MMD PDF** = GPL-3.0 fork of KillerPDF 1.7.5, staying GPL-3.0, `FORK-NOTICE.md`
in the repo records what changed.
---
## What was done
### Chosen and proven (this part is solid)
- Surveyed open-source Windows PDF editors. **No MIT-licensed app does true
inline PDF text editing** — the only genuinely MIT desktop option (every-pdf)
can only stamp text on top of a page. KillerPDF (GPL-3.0) was picked; GPL
restricts forking and redistribution, not internal deployment to staff.
- **Passed the check that killed PDFgear.** `.pdf` UserChoice ProgId and Hash
identical before and after install and after opening a PDF; the protective
deny ACE intact; one signed executable, no helper binary. Read live from the
registry on our own machine.
- **Found and fixed a real defect.** Editing existing text left the original
glyphs in the file — a delivery note corrected from 12 to 17 tonnes returned
*both* figures from `pdftotext`. `Services/PdfRedactText.cs` now walks the
page content stream, tracks the CTM and text matrix, and blanks every
show-text operator whose origin sits inside the cover rectangle, before any
new content is appended. Proven: after the fix the saved file contains only
the new figure, every other line intact and still searchable.
- **Built the test rig that found it.** `/opt/mmd-wintest/wintest` on the
hypervisor: Xvfb + xfreerdp3 + xdotool, so every action is a real pointer or
key event with no scripting back door. This is how user testing is done from
now on.
### Dedicated Windows test machine
VM 203 on **mmd-fsn-03** renamed **mmd-win-test-01**, `onboot=1`, described in
Proxmox. Windows Server 2022, 20 cores / 72 GB, guest 10.10.20.22. Its DNS was
broken (no internet at all) — fixed. .NET SDK 8 installed for builds.
It still also carries the Mali plant-station build, so it is shared.
### Design — failed review, not accepted
The rebrand and the new MMD shell design system ("red plate") were applied and
merged **before any design existed**, which is the wrong order and Ben called
it. Redone properly afterwards: design system pulled from the Claude Design
project, red-plate stylesheet lifted verbatim from the session that owns it,
seven screens composed at 800×600, then put through an **independent adversarial
review** — the gate that was missing.
**Verdict: DESIGN-FAIL.** 3 blockers, 5 major, 3 minor. All eleven findings and
the fix state are in `docs/mmd-pdf/SIGNOFFS.md`.
---
## Current live / verified state
| Thing | State |
|---|---|
| `main` | carries the rebrand + the redaction fix (PR #1, merged) |
| `design/official-logo-and-tokens-20260827` | **bfb9845, pushed, NOT merged** — official logo + design-system values. Unaccepted: the design failed review. |
| Build | `dotnet build -c Release`, 0 errors, on mmd-win-test-01 |
| App | runs; opens, edits and saves correctly, keyboard-and-mouse tested |
| Design | **DESIGN-FAIL**, no re-review run, no Ben sign-off |
| Scanning | not written |
| Signing / Intune | not started |
Claude Design project `446a3931-c345-48da-a3ac-246af02a2e52` ("Design System"):
the repo's live CDS 0.3.40 was pushed into `mmd-portal/` (its mirror was stale
at 0.3.11), and round-1 design pages into `mmd-pdf/`. **Round 2 was not pushed**
— it failed review.
Two artifact links exist from before Ben's `/login` and are **dead**
(`65c7726d…`, `e47d705f…`). Live under the current account:
`7d8d6b56-ef36-4729-8e88-a3fb1392707e` (round-1 canvas) and
`a7a974e8-aff4-49e6-b4cc-f80414769515` (status page). Both are round-1 and both
predate the review — do not present either as reviewed.
---
## Resume steps
1. **Read** `docs/mmd-pdf/SIGNOFFS.md` for the eleven findings, then
`BACKLOG.md` for what is ⛔.
2. **Get Ben's toolbar pick** — A, B or C on the Directions board. The
independent reviewer recommends **C** (three tabs; nothing hidden, every tool
named). Nothing else in the design can be finished without it.
3. **Apply the seven unapplied findings** — 1, 6, 7, 8, 9, 10, 11 — the big one
being the rebuild of Main / EditText / PageRail / Nothing onto Direction C.
Working files: `docs/mmd-pdf/design/build.py`, regenerate with `python3
build.py`, render each `.dc.html` locally and look at it.
4. **Re-run the independent review** with the prompt in
`docs/mmd-pdf/TEST-CASES.md` under "To review a design". Give the reviewer
the **complete** palette this time, including `#FAFAF8` and `#C9C6BC` — my
incomplete brief caused half of finding 4.
5. **Only after DESIGN-PASS**, publish the canvas and hand Ben the link for
annotation. His annotation closes the gate; the reviewer's PASS does not.
6. **Then** re-derive `Themes/MMD.xaml` from the signed-off design and merge
`design/official-logo-and-tokens-20260827`.
7. **Then** scanning (NAPS2.Wia, MIT, .NET Framework 4.6.2 — compatible with
this net48 codebase), then signing, then Intune.
---
## Key facts, commits, addresses
- Repo `gitea.baobab-ts.com/mmd-it/mmd-pdf`; `main` = PR #1 merged;
branch `design/official-logo-and-tokens-20260827` = **bfb9845**.
- Hypervisor **mmd-fsn-03** — Hetzner dedicated, Falkenstein FSN1-DC16, auction
#3049905, public 168.119.88.172, tailnet 100.64.0.19, Proxmox VE 9.2.10.
Reach it with `ssh -i ~/.ssh/id_ed25519_mmd_fsn03_rescue root@100.64.0.19`.
- Test machine **mmd-win-test-01** (VM 203), guest 10.10.20.22. Humans reach it
in a browser at `https://plantpc.baobab-ts.com` (Guacamole, Entra SSO).
- Source on the VM at `C:\src`; build output
`C:\src\bin\Release\net48\MmdPdf.exe`; .NET SDK at `C:\build\dotnet`.
- Graph for Intune/Entra: **MMD-CIO-Agent client secret** at
`~/mmd-projects/entra-cio-agent/secret.env`, helper `graphlib.mjs`. The
certificate in `~/.config/mmd/certs` is Exchange PowerShell only — do not
reach for it and then conclude the agent lacks Intune access.
---
## Open items
- **Ben: pick the toolbar direction.** Blocks everything else in the design.
- **Ben: the 1624px app icon.** No official small MMD mark exists anywhere. The
icon crops the lockup's baobab disc, which reads from 32px and is muddy below
it. An agent must not draw a simplified one. Commission or accept muddy.
- Seven design findings unapplied; no re-review run.
- Scanning not written. Signing not started. Intune not started.
- mmd-win-test-01's Windows evaluation licence — a restart reset it to 180 days,
but it needs a real licence before it is a standing fixture.
- Design round 2 not pushed to the Claude Design project (it failed review).
---
## Gotchas
1. **`Stop-Process` over WinRM against a running GUI app hangs the session.**
Launch GUI apps with a *new* scheduled-task name instead.
2. **`freerdp2-x11` does not exist on Debian 13.** It is `freerdp3-x11`, and the
binary is `xfreerdp3`, not `xfreerdp`.
3. **The word-mark was two literal `<Run Text="Killer">` elements**, not the
product-name token — a global rename misses it. Check the title bar visually
after any rename.
4. **`redplate.css` belongs to another session.** It is lifted verbatim from
their `build.mjs`. I edited it once chasing a review finding and reverted it.
Do not edit it; raise a discrepancy with the owning session instead.
5. **A `/login` mid-session orphans artifacts** published earlier under the old
account. Republishing the same *file path* keeps mapping to the dead one —
copy the file to a new path to claim a fresh URL.
6. **`Ctrl+A` inside an inline text edit selects all annotations**, not the
text. Use `End` + `BackSpace`. `Enter` commits, `Escape` cancels — the status
bar now says so because both were guessed wrong.
7. **Flatten is not redaction's friend.** It removes the old text by turning the
page into a picture, destroying every searchable word. That is why the
content-stream fix exists.
8. **The design gate is not closed by a reviewer's PASS.** Only Ben's
annotation closes it, and design that never went through the proper lane is
an automatic fail however good it looks.
+58 -6
View File
@@ -112,12 +112,12 @@
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Width" Value="36"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
@@ -231,6 +231,49 @@
</Setter>
</Style>
<!-- Solid tool button on the band: flat, 1px border, 4px corner. The one place a
white fill appears on the red ground, so the tool it carries reads as the
action always within reach. -->
<Style x:Key="BandButton" TargetType="Button">
<Setter Property="Background" Value="#FFFFFF"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderBrush" Value="#FFFFFF"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,0"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Margin" Value="0,0,10,0"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="10.5"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F1F1EC"/>
<Setter Property="BorderBrush" Value="#F1F1EC"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#E4E3DC"/>
<Setter Property="BorderBrush" Value="#E4E3DC"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.55"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ChromeButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource CaptionGlyphBrush}"/>
@@ -1179,10 +1222,10 @@
<!-- Soft drop shadow: a blurred, offset copy behind the crisp wordmark -->
<StackPanel Orientation="Horizontal" Margin="1,1,0,0" Opacity="{DynamicResource WordmarkShadowOpacity}">
<StackPanel.Effect><BlurEffect Radius="2"/></StackPanel.Effect>
<TextBlock><Run Text="MMD " FontFamily="{DynamicResource WordmarkFont}" FontSize="16.5" Foreground="#D8000000"/><Run Text="PDF" FontFamily="{DynamicResource WordmarkFontPdf}" FontWeight="Bold" FontSize="21.45" Foreground="#D8000000"/></TextBlock>
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock><Run Text="MMD " FontFamily="{DynamicResource WordmarkFont}" FontSize="16.5" Foreground="{DynamicResource TextBrush}"/><Run Text="PDF" FontFamily="{DynamicResource WordmarkFontPdf}" FontWeight="Bold" FontSize="21.45" Foreground="{DynamicResource AccentLogo}"/></TextBlock>
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
</StackPanel>
</Grid>
</StackPanel>
@@ -1192,6 +1235,15 @@
Margin="16,0,12,0" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource FilenameBrush}" FontFamily="Consolas" FontSize="11"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="{DynamicResource CaptionButtonsMargin}">
<Button x:Name="BandScanBtn" Click="Scan_Click" Style="{StaticResource BandButton}"
ToolTip="Pull pages off a copier"
WindowChrome.IsHitTestVisibleInChrome="True">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8FE;" FontFamily="Segoe MDL2 Assets" FontSize="12"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Scan" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Click="MinimizeBtn_Click" Style="{StaticResource ChromeButton}"
Foreground="{DynamicResource CaptionGlyphBrush}"
WindowChrome.IsHitTestVisibleInChrome="True">
@@ -1237,7 +1289,7 @@
<Border IsHitTestVisible="False" Opacity="{DynamicResource GrainOpacity}" Background="{StaticResource GrainBrushShared}"/>
<!-- Background=Transparent is load-bearing: without it the grid's empty stretches have no
hit surface and the right-click appearance picker only opens over the buttons. -->
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,2" Background="Transparent">
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,1" Background="Transparent">
<Grid.ContextMenu>
<!-- Items are built in code (BuildToolbarMenu, SettingsPanel.cs) so captions re-Loc on
language switch; attached here so WPF supplies the inheritance context that lets
+16
View File
@@ -532,6 +532,22 @@ namespace MmdPdf
// squared chrome on every move (and once now that the handle exists).
LocationChanged += OnWindowLocationChanged;
UpdateWindowChrome();
FitToSmallScreen();
}
/// <summary>
/// Plant machines run at 800 by 600. The default 1000 by 700 window does not fit one, and
/// a window taller than the screen puts its own title bar - and therefore Scan, the window
/// buttons and the tabs - above the top edge where nobody can reach them. On a screen that
/// cannot hold the default, start maximised instead.
/// </summary>
private void FitToSmallScreen()
{
if (WindowState != WindowState.Normal) return;
var work = System.Windows.SystemParameters.WorkArea;
if (work.Width <= 0 || work.Height <= 0) return;
if (Width <= work.Width && Height <= work.Height) return;
WindowState = WindowState.Maximized;
}
// ============================================================
+2
View File
@@ -134,6 +134,8 @@
<ItemGroup>
<Resource Include="Resources\mmd-icon.ico" />
<Resource Include="Resources\mmd-icon.png" />
<Resource Include="Resources\mmd-lockup-red.png" />
<Resource Include="Resources\mmd-lockup-white.png" />
<Resource Include="Fonts\Typewriter-A602.ttf" />
<EmbeddedResource Include="Resources\pdf-file.ico" />
</ItemGroup>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

+373
View File
@@ -0,0 +1,373 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
namespace MmdPdf.Services
{
/// <summary>Which side of the paper the copier reads, and from where.</summary>
internal enum ScanFeed
{
Flatbed,
FeederOneSide,
FeederBothSides,
}
/// <summary>How much ink the copier records.</summary>
internal enum ScanColour
{
BlackAndWhite,
Greyscale,
Colour,
}
/// <summary>One copier the machine can reach.</summary>
internal sealed class ScannerInfo
{
internal ScannerInfo(string deviceId, string name)
{
DeviceId = deviceId;
Name = name;
}
internal string DeviceId { get; }
internal string Name { get; }
public override string ToString() => Name;
}
/// <summary>
/// Pulls pages off a network copier or a desk scanner through WIA, the imaging service that
/// ships with Windows. Nothing is installed for this: the COM objects are created late-bound
/// by ProgID, so a machine with no imaging stack fails at <see cref="IsAvailable"/> instead of
/// at load time, and the application still starts.
///
/// The page images land in a folder of their own under the user's temp directory and are
/// handed back as file paths; turning them into a document is PdfImport's job, the same way
/// an imported photograph is handled.
/// </summary>
internal static class ScanService
{
// WIA item property ids. These are the documented constants; the COM interface takes
// them as integers, so they are written here once rather than repeated as magic numbers.
private const int WIA_IPS_CUR_INTENT = 6146;
private const int WIA_IPS_XRES = 6147;
private const int WIA_IPS_YRES = 6148;
private const int WIA_IPS_XPOS = 6149;
private const int WIA_IPS_YPOS = 6150;
private const int WIA_IPS_XEXTENT = 6151;
private const int WIA_IPS_YEXTENT = 6152;
private const int WIA_IPA_DATATYPE = 4103;
private const int WIA_IPA_DEPTH = 4104;
// Device properties that drive the sheet feeder.
private const int WIA_DPS_DOCUMENT_HANDLING_SELECT = 3088;
private const int WIA_DPS_DOCUMENT_HANDLING_STATUS = 3087;
private const int WIA_DPS_PAGES = 3096;
private const int FEEDER = 0x001;
private const int FLATBED = 0x002;
private const int DUPLEX = 0x004;
private const int FEED_READY = 0x001;
// Intent values: what the copier should optimise for.
private const int INTENT_COLOUR = 0x00000001;
private const int INTENT_GREYSCALE = 0x00000002;
private const int INTENT_TEXT = 0x00000004;
private const string FORMAT_PNG = "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}";
// Paper the copier is asked for, in inches. A4 is the MMD standard; Letter is here
// because the Dubai office buys it locally.
private static readonly Dictionary<string, (double W, double H)> PaperInches =
new Dictionary<string, (double, double)>(StringComparer.OrdinalIgnoreCase)
{
["A4 portrait"] = (8.27, 11.69),
["A4 landscape"] = (11.69, 8.27),
["A5 portrait"] = (5.83, 8.27),
["Letter portrait"] = (8.5, 11.0),
["Legal portrait"] = (8.5, 14.0),
};
internal static IEnumerable<string> PaperSizes => PaperInches.Keys;
/// <summary>True when this machine has the Windows imaging service at all.</summary>
internal static bool IsAvailable => Type.GetTypeFromProgID("WIA.DeviceManager") != null;
/// <summary>
/// Every copier and scanner the machine can currently reach. An empty list is a normal
/// answer - it means nothing is switched on - and never an exception the caller has to
/// catch.
/// </summary>
internal static List<ScannerInfo> ListScanners()
{
var found = new List<ScannerInfo>();
var progId = Type.GetTypeFromProgID("WIA.DeviceManager");
if (progId == null) return found;
object? manager = null;
try
{
manager = Activator.CreateInstance(progId);
dynamic dm = manager!;
dynamic infos = dm.DeviceInfos;
int count = (int)infos.Count;
for (int i = 1; i <= count; i++) // WIA collections are 1-based
{
dynamic info = infos[i];
try
{
// 1 == ScannerDeviceType. Cameras and video devices are skipped.
if ((int)info.Type != 1) continue;
string name = ReadProperty(info.Properties, "Name") ?? "Copier";
found.Add(new ScannerInfo((string)info.DeviceID, name));
}
finally { Release(info); }
}
}
catch (COMException) { /* imaging service not running: nothing to offer */ }
catch (InvalidCastException) { }
finally { Release(manager); }
return found;
}
/// <summary>
/// Reads pages and writes each one as a PNG into its own folder. Returns the files in
/// page order. Cancelling stops after the page in progress - a copier cannot be
/// interrupted mid-sheet.
/// </summary>
/// <param name="onPage">Called with the running page count as each sheet lands.</param>
internal static List<string> Acquire(
string deviceId,
ScanFeed feed,
ScanColour colour,
int dpi,
string paper,
int maxPages,
CancellationToken cancel,
Action<int>? onPage = null)
{
if (string.IsNullOrWhiteSpace(deviceId)) throw new ArgumentException("No copier chosen.", nameof(deviceId));
if (dpi < 75 || dpi > 1200) throw new ArgumentOutOfRangeException(nameof(dpi));
var progId = Type.GetTypeFromProgID("WIA.DeviceManager")
?? throw new InvalidOperationException("This machine has no imaging service.");
string folder = Path.Combine(Path.GetTempPath(), "MmdPdf.Scan." + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(folder);
var pages = new List<string>();
object? manager = null;
object? device = null;
try
{
manager = Activator.CreateInstance(progId);
dynamic dm = manager!;
dynamic infos = dm.DeviceInfos;
dynamic? chosen = null;
int count = (int)infos.Count;
for (int i = 1; i <= count; i++)
{
dynamic info = infos[i];
if (string.Equals((string)info.DeviceID, deviceId, StringComparison.Ordinal)) { chosen = info; break; }
Release(info);
}
if (chosen == null) throw new InvalidOperationException("That copier is no longer reachable.");
device = chosen.Connect();
dynamic dev = device!;
ConfigureFeeder(dev, feed, maxPages);
dynamic item = dev.Items[1];
try
{
ConfigurePage(item, colour, dpi, paper);
for (int page = 1; page <= maxPages; page++)
{
cancel.ThrowIfCancellationRequested();
string path = Path.Combine(folder, "page-" + page.ToString("000", CultureInfo.InvariantCulture) + ".png");
try
{
dynamic image = item.Transfer(FORMAT_PNG);
try
{
image.SaveFile(path);
pages.Add(path);
onPage?.Invoke(pages.Count);
}
finally { Release(image); }
}
catch (COMException ex) when (IsOutOfPaper(ex))
{
break; // the feeder emptied: a normal end, not a failure
}
if (feed == ScanFeed.Flatbed) break; // one sheet on the glass
if (!FeederHasPaper(dev)) break;
}
}
finally { Release(item); }
}
finally
{
Release(device);
Release(manager);
}
if (pages.Count == 0)
{
try { Directory.Delete(folder, true); } catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
return pages;
}
private static void ConfigureFeeder(dynamic device, ScanFeed feed, int maxPages)
{
int handling = feed == ScanFeed.Flatbed ? FLATBED : FEEDER;
if (feed == ScanFeed.FeederBothSides) handling |= DUPLEX;
TrySetProperty(device.Properties, WIA_DPS_DOCUMENT_HANDLING_SELECT, handling);
if (feed != ScanFeed.Flatbed) TrySetProperty(device.Properties, WIA_DPS_PAGES, maxPages);
}
private static void ConfigurePage(dynamic item, ScanColour colour, int dpi, string paper)
{
int intent = colour switch
{
ScanColour.Colour => INTENT_COLOUR,
ScanColour.Greyscale => INTENT_GREYSCALE,
_ => INTENT_TEXT,
};
int depth = colour switch
{
ScanColour.Colour => 24,
ScanColour.Greyscale => 8,
_ => 1,
};
int dataType = colour switch
{
ScanColour.Colour => 3, // WIA_DATA_COLOR
ScanColour.Greyscale => 2, // WIA_DATA_GRAYSCALE
_ => 0, // WIA_DATA_THRESHOLD
};
TrySetProperty(item.Properties, WIA_IPS_CUR_INTENT, intent);
TrySetProperty(item.Properties, WIA_IPA_DATATYPE, dataType);
TrySetProperty(item.Properties, WIA_IPA_DEPTH, depth);
TrySetProperty(item.Properties, WIA_IPS_XRES, dpi);
TrySetProperty(item.Properties, WIA_IPS_YRES, dpi);
var size = PaperInches.TryGetValue(paper ?? string.Empty, out var found) ? found : PaperInches["A4 portrait"];
TrySetProperty(item.Properties, WIA_IPS_XPOS, 0);
TrySetProperty(item.Properties, WIA_IPS_YPOS, 0);
TrySetProperty(item.Properties, WIA_IPS_XEXTENT, (int)Math.Round(size.W * dpi));
TrySetProperty(item.Properties, WIA_IPS_YEXTENT, (int)Math.Round(size.H * dpi));
}
private static bool FeederHasPaper(dynamic device)
{
try
{
object? raw = ReadPropertyValue(device.Properties, WIA_DPS_DOCUMENT_HANDLING_STATUS);
if (raw == null) return false;
return ((int)Convert.ToInt32(raw, CultureInfo.InvariantCulture) & FEED_READY) == FEED_READY;
}
catch (COMException) { return false; }
catch (InvalidCastException) { return false; }
}
/// <summary>The copier saying "the tray is empty", which ends a run rather than failing it.</summary>
private static bool IsOutOfPaper(COMException ex)
{
const int WIA_ERROR_PAPER_EMPTY = unchecked((int)0x80210003);
const int WIA_ERROR_PAPER_JAM = unchecked((int)0x80210002);
return ex.ErrorCode == WIA_ERROR_PAPER_EMPTY || ex.ErrorCode == WIA_ERROR_PAPER_JAM;
}
private static string? ReadProperty(dynamic properties, string name)
{
try
{
int count = (int)properties.Count;
for (int i = 1; i <= count; i++)
{
dynamic p = properties[i];
try
{
if (string.Equals((string)p.Name, name, StringComparison.OrdinalIgnoreCase))
return Convert.ToString(p.get_Value(), CultureInfo.InvariantCulture);
}
finally { Release(p); }
}
}
catch (COMException) { }
catch (InvalidCastException) { }
return null;
}
private static object? ReadPropertyValue(dynamic properties, int propertyId)
{
try
{
int count = (int)properties.Count;
for (int i = 1; i <= count; i++)
{
dynamic p = properties[i];
try
{
if ((int)p.PropertyID == propertyId) return p.get_Value();
}
finally { Release(p); }
}
}
catch (COMException) { }
catch (InvalidCastException) { }
return null;
}
/// <summary>
/// Sets one property if the copier has it. Copiers differ wildly in what they expose, so a
/// missing or read-only property is ignored rather than failing the run - the copier then
/// uses its own default for that setting.
/// </summary>
private static void TrySetProperty(dynamic properties, int propertyId, int value)
{
try
{
int count = (int)properties.Count;
for (int i = 1; i <= count; i++)
{
dynamic p = properties[i];
try
{
if ((int)p.PropertyID != propertyId) continue;
if ((bool)p.IsReadOnly) return;
p.set_Value(value);
return;
}
finally { Release(p); }
}
}
catch (COMException) { }
catch (InvalidCastException) { }
}
private static void Release(object? comObject)
{
if (comObject == null) return;
try
{
if (Marshal.IsComObject(comObject)) Marshal.ReleaseComObject(comObject);
}
catch (ArgumentException) { }
catch (InvalidComObjectException) { }
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Windows;
using MmdPdf.Controls;
using MmdPdf.Features.Scan;
namespace MmdPdf
{
// Pulling pages off a copier. The dialog does the talking to the hardware; this file only
// turns the pages it hands back into a document, which is the same route an imported
// photograph takes (ImportAndZip.cs).
public partial class MainWindow
{
private void Scan_Click(object sender, RoutedEventArgs e)
{
var dlg = new ScanDialog { Owner = this };
bool? answer;
try
{
answer = dlg.ShowDialog();
}
catch (Exception ex)
{
KillerDialog.Show(this, "The copier could not be reached.\n" + ex.Message,
"MMD PDF", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (answer != true || dlg.Pages.Length == 0) return;
string name = "Scan " + DateTime.Now.ToString("d MMM yyyy HH:mm", CultureInfo.CurrentCulture);
OpenImagesAsImportedTab(dlg.Pages, name);
// "Make the words searchable" is the copier setting a person ticks before the run;
// the reading itself happens here, on the document that just opened, through the
// text recognition the application already carries.
if (dlg.MakeSearchable) MakeSearchablePdf();
}
}
}
+3 -3
View File
@@ -590,7 +590,7 @@ namespace MmdPdf
// Text only: caption, no icon (nothing to shed - there'd be nothing left).
if (textOnly)
{
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
btn.Content = new TextBlock
{
Text = Loc(key),
@@ -604,7 +604,7 @@ namespace MmdPdf
// Text beside the icon, while it still fits.
if (beside && withLabel)
{
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
var row = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
row.Children.Add(new TextBlock
{
@@ -628,7 +628,7 @@ namespace MmdPdf
// Text under the icon: the icon stacked over a small caption, while it still fits.
if (under && withLabel)
{
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 56 : 52; btn.Padding = new Thickness(6, 4, 6, 4);
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 45 : 42; btn.Padding = new Thickness(6, 2, 6, 2);
var col = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
col.Children.Add(new TextBlock
{
+84 -43
View File
@@ -3,17 +3,55 @@
xmlns:po="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<!-- MMD Steel Group corporate theme. The only theme this application ships.
Palette is the Employee Portal's: C8102E red used sparingly, 23221E ink,
F1F1EC platinum ground, FFFFFF paper. Square edges, no grain, no shadows. -->
<!-- MMD Steel Group corporate theme - the only theme this application ships.
Values are the new MMD shell design system ("red plate", light-only, no dark
variant): white work area, silver chrome, desk grey behind the document,
23221E ink, C8102E red partitioned to primary action / focus / urgency only.
Square by default. Held for Ben's sign-off: dark table header rows and the
window-chrome variant (V2 vs V7) are still being chosen. -->
<!-- The one merged top bar, locked 2026-08-28. MMD red, the white lockup on it,
and the window buttons at its right end. These are the design system's own
values; no colour here is new. -->
<LinearGradientBrush x:Key="TitleBarBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
<GradientStop Offset="0.0" Color="#d4213f"/>
<GradientStop Offset="0.55" Color="#c8102e"/>
<GradientStop Offset="1.0" Color="#a50d26"/>
</LinearGradientBrush>
<sys:Double x:Key="TitleBarHeight">42</sys:Double>
<Thickness x:Key="TitleBarPadding">14,0,0,0</Thickness>
<SolidColorBrush x:Key="CaptionGlyphBrush" Color="#ffffff" po:Freeze="True"/>
<sys:Double x:Key="CaptionButtonWidth">24</sys:Double>
<sys:Double x:Key="CaptionButtonHeight">24</sys:Double>
<Thickness x:Key="CaptionButtonsMargin">0,0,9,0</Thickness>
<!-- The window buttons sit on the red band, so they are white. The close button
inverts on hover - white plate, red cross - because red on red is invisible,
which is exactly what the first build of this band showed. -->
<SolidColorBrush x:Key="CaptionButtonBrush" Color="#1fffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="CaptionHoverBrush" Color="#33ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="CaptionCloseBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="CaptionCloseHoverBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="CaptionCloseHoverFgBrush" Color="#c8102e" po:Freeze="True"/>
<sys:Double x:Key="WordmarkShadowOpacity">0</sys:Double>
<sys:Double x:Key="TitleIconSize">0</sys:Double>
<Thickness x:Key="TitleIconMargin">0</Thickness>
<!-- A window that is in front, for the dialogs: white to silver with a red top edge.
The bevel lives on title bars only; buttons are flat. -->
<LinearGradientBrush x:Key="WindowFrontTitleBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
<GradientStop Offset="0.0" Color="#ffffff"/>
<GradientStop Offset="0.62" Color="#ffffff"/>
<GradientStop Offset="1.0" Color="#f1f1ec"/>
</LinearGradientBrush>
<sys:Double x:Key="FooterHeight">26</sys:Double>
<!-- Surfaces -->
<SolidColorBrush x:Key="SurfaceBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="PaneBrush" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="RowHoverBrush" Color="#ecece6" po:Freeze="True"/>
<SolidColorBrush x:Key="MenuHoverBrush" Color="#ecece6" po:Freeze="True"/>
<SolidColorBrush x:Key="BackgroundBrush" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="BgRecentPanel" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="SurfaceBrush" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="PaneBrush" Color="#e4e3dc" po:Freeze="True"/>
<SolidColorBrush x:Key="RowHoverBrush" Color="#f7f7f3" po:Freeze="True"/>
<SolidColorBrush x:Key="MenuHoverBrush" Color="#f7f7f3" po:Freeze="True"/>
<SolidColorBrush x:Key="BackgroundBrush" Color="#f3f3f0" po:Freeze="True"/>
<SolidColorBrush x:Key="BgRecentPanel" Color="#f3f3f0" po:Freeze="True"/>
<SolidColorBrush x:Key="BgFlyout" Color="#ffffff" po:Freeze="True"/>
<LinearGradientBrush x:Key="TabStripFadeBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
<GradientStop Offset="0.0" Color="#0023221e"/>
@@ -21,12 +59,12 @@
<GradientStop Offset="1.0" Color="#2223221e"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="MenuBackgroundBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="CardBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="CardBorderBrush" Color="#e6e5df" po:Freeze="True"/>
<!-- Accent. Red carries the active tool and the one primary action, nothing else. -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#c8102e" po:Freeze="True"/>
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#ad0e28" po:Freeze="True"/>
<SolidColorBrush x:Key="PrimaryPressedBrush" Color="#920c21" po:Freeze="True"/>
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#a80d24" po:Freeze="True"/>
<SolidColorBrush x:Key="PrimaryPressedBrush" Color="#8e0b20" po:Freeze="True"/>
<SolidColorBrush x:Key="OnPrimaryBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="OutlineBtnBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="HeaderLineBrush" Color="#23221e" po:Freeze="True"/>
@@ -35,39 +73,39 @@
<SolidColorBrush x:Key="InstallBtnBg" Color="#f1f1ec" po:Freeze="True"/>
<!-- Borders -->
<SolidColorBrush x:Key="MenuBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="DropBorder" Color="#b9b9b0" po:Freeze="True"/>
<SolidColorBrush x:Key="PaneBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="MenuBorderBrush" Color="#d7d6ce" po:Freeze="True"/>
<SolidColorBrush x:Key="DropBorder" Color="#b9b8ae" po:Freeze="True"/>
<SolidColorBrush x:Key="PaneBorderBrush" Color="#d7d6ce" po:Freeze="True"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#b9b8ae" po:Freeze="True"/>
<!-- Selection: dark on light, the way the portal tables read. -->
<SolidColorBrush x:Key="RowSelectedBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="OutlinePressedBrush" Color="#f6e0e4" po:Freeze="True"/>
<SolidColorBrush x:Key="OutlinePressedBrush" Color="#fbeef0" po:Freeze="True"/>
<SolidColorBrush x:Key="SelectionBg" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="SelectionFg" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="SettingsOpenRowBg" Color="#ecece6" po:Freeze="True"/>
<SolidColorBrush x:Key="SettingsOpenRowBg" Color="#f1f1ec" po:Freeze="True"/>
<!-- Type -->
<SolidColorBrush x:Key="TextBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="MutedTextBrush" Color="#5c5a54" po:Freeze="True"/>
<SolidColorBrush x:Key="MutedTextBrush" Color="#3c3a34" po:Freeze="True"/>
<SolidColorBrush x:Key="MenuTextBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="ChromeTextBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="FilenameBrush" Color="#5c5a54" po:Freeze="True"/>
<SolidColorBrush x:Key="DimTextBrush" Color="#6f6d66" po:Freeze="True"/>
<SolidColorBrush x:Key="TextFooter" Color="#8b8880" po:Freeze="True"/>
<SolidColorBrush x:Key="ChromeTextBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="FilenameBrush" Color="#e8ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="DimTextBrush" Color="#767368" po:Freeze="True"/>
<SolidColorBrush x:Key="TextFooter" Color="#6b6a64" po:Freeze="True"/>
<SolidColorBrush x:Key="DangerRed" Color="#c8102e" po:Freeze="True"/>
<!-- Furniture -->
<SolidColorBrush x:Key="BgDragHandle" Color="#b9b9b0" po:Freeze="True"/>
<SolidColorBrush x:Key="DragLine" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="BgDragHandle" Color="#c7c6bd" po:Freeze="True"/>
<SolidColorBrush x:Key="DragLine" Color="#d7d6ce" po:Freeze="True"/>
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#5523221e" po:Freeze="True"/>
<SolidColorBrush x:Key="ScrollThumbHoverBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="TableHeaderBrush" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="RowAltBrush" Color="#0a23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="InputBorderBrush" Color="#c9c9c1" po:Freeze="True"/>
<SolidColorBrush x:Key="RowAltBrush" Color="#0823221e" po:Freeze="True"/>
<SolidColorBrush x:Key="InputBorderBrush" Color="#c7c6bd" po:Freeze="True"/>
<SolidColorBrush x:Key="InputHoverBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="TreeLineBrush" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="SliderTrack" Color="#d9d9d2" po:Freeze="True"/>
<SolidColorBrush x:Key="TreeLineBrush" Color="#e6e5df" po:Freeze="True"/>
<SolidColorBrush x:Key="SliderTrack" Color="#d7d6ce" po:Freeze="True"/>
<SolidColorBrush x:Key="RadioAccent" Color="#c8102e" po:Freeze="True"/>
<SolidColorBrush x:Key="BgOverlay" Color="#8823221e" po:Freeze="True"/>
@@ -77,29 +115,32 @@
<sys:Double x:Key="HeaderShadowOpacity">0</sys:Double>
<!-- The workspace behind the page. Platinum, one step darker than the page itself. -->
<SolidColorBrush x:Key="BgCanvas" Color="#e4e4de" po:Freeze="True"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#f6e0e4" po:Freeze="True"/>
<SolidColorBrush x:Key="BgCanvas" Color="#d4d0c6" po:Freeze="True"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#fbeef0" po:Freeze="True"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#ecece6" po:Freeze="True"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#23221e" po:Freeze="True"/>
<!-- Shortcut keyboard view. Graded ink rather than a rainbow; red marks editing only. -->
<SolidColorBrush x:Key="KsCatFile" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatTools" Color="#5c5a54" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatTools" Color="#3c3a34" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatEdit" Color="#c8102e" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatNav" Color="#6f6d66" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatView" Color="#8b8880" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatSearch" Color="#5c5a54" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatHelp" Color="#8b8880" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatOcr" Color="#6f6d66" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatNav" Color="#767368" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatView" Color="#6b6a64" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatSearch" Color="#3c3a34" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatHelp" Color="#6b6a64" po:Freeze="True"/>
<SolidColorBrush x:Key="KsCatOcr" Color="#767368" po:Freeze="True"/>
<!-- No text stroke on a light corporate surface. -->
<DropShadowEffect x:Key="TextStroke" Color="Black" BlurRadius="0" ShadowDepth="0" Direction="270" Opacity="0"/>
<!-- Square. The portal's chrome is square-edged; rounded corners are not MMD. -->
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
<CornerRadius x:Key="SmallCornerRadius">0</CornerRadius>
<CornerRadius x:Key="FlyoutCornerRadius">0</CornerRadius>
<CornerRadius x:Key="AccentSwatchCornerRadius">0</CornerRadius>
<!-- Four-pixel corners, per the design system locked 2026-08-28. The window itself
stays square at the top because the band runs edge to edge when maximised, which
is how these machines are used. -->
<CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
<CornerRadius x:Key="SmallCornerRadius">4</CornerRadius>
<CornerRadius x:Key="FlyoutCornerRadius">4</CornerRadius>
<CornerRadius x:Key="AccentSwatchCornerRadius">4</CornerRadius>
<CornerRadius x:Key="TitleBarCornerRadius">0</CornerRadius>
</ResourceDictionary>
+42
View File
@@ -0,0 +1,42 @@
# MMD PDF — feature backlog
Status: 🟢 proven end-to-end on a real build with evidence · 🟡 in progress ·
⚪ not started · ⛔ blocked. A green build or a passing unit test is not 🟢.
| ID | Feature | Ver | Owner | Status | Sign-off | PRD / Screen / Test |
|---|---|---|---|---|---|---|
| PDF-01 | Open and read a PDF | 1.7.5 | kua-agent | 🟢 | unsigned | PRD · SCREENS Main · TC-01 |
| PDF-02 | Change text already on the page | 1.7.5 | kua-agent | 🟢 | unsigned | PRD · SCREENS EditText · TC-02 |
| PDF-03 | **The old text is removed, not covered** | 0.1 | kua-agent | 🟢 | unsigned | PRD req.1 · SCREENS Save · TC-03 |
| PDF-04 | Save over the original, with confirmation | 1.7.5 | kua-agent | 🟢 | unsigned | SCREENS Save · TC-04 |
| PDF-05 | Runs with no admin rights, portable | 1.7.5 | kua-agent | 🟢 | unsigned | PRD req.4 · TC-05 |
| PDF-06 | Leaves the Windows default-PDF setting alone | 1.7.5 | kua-agent | 🟢 | unsigned | PRD "why" · TC-06 |
| PDF-07 | MMD name, icon and word-mark | 0.1 | kua-agent | 🟡 | unsigned | SCREENS all · TC-07 |
| PDF-08 | MMD shell design system, light only, no theme picker | 0.1 | kua-agent | ⛔ | unsigned | PRD req.3 · SCREENS · TC-08 |
| PDF-09 | Works at 800×600 | 0.1 | — | ⛔ | unsigned | PRD req.2 · SCREENS Directions · TC-09 |
| PDF-10 | Fill and sign forms | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-10 |
| PDF-11 | Mark up: highlight, draw, shapes, stamps, images | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-11 |
| PDF-12 | Page work: merge, extract, delete, reorder, crop | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-12 |
| PDF-13 | Turn a scan into searchable text | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-13 |
| PDF-14 | Pull pages off a network copier | — | — | ⚪ | unsigned | PRD in-scope · SCREENS Scan · TC-14 |
| PDF-15 | French interface | 1.7.5 | — | ⚪ | unsigned | PRD req.5 · TC-15 |
| PDF-16 | Print | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-16 |
| PDF-17 | Code signing under an MMD certificate | — | — | ⚪ | unsigned | PRD open q. · TC-17 |
| PDF-18 | Deploy through Intune | — | — | ⚪ | unsigned | PRD open q. · TC-18 |
| PDF-19 | Official square app mark for 1624px | — | — | ⛔ | unsigned | PRD open q. |
## Why the ⛔
- **PDF-08, PDF-09** — the design failed an independent review on 2026-08-27
(3 blockers, 5 major). Nothing may be built on it until it passes and Ben
signs it. Findings and the fix state are in `SIGNOFFS.md`.
- **PDF-19** — no official small mark exists anywhere in MMD's artwork, and only
Ben can authorise commissioning one. An agent must not draw it.
## Evidence behind the 🟢 rows
All on **mmd-win-test-01** (VM 203, hypervisor mmd-fsn-03, Falkenstein),
2026-08-27, driven by keyboard and mouse only through
`/opt/mmd-wintest/wintest`. Screenshots in `design/shots/` are design mockups,
not this evidence; the run itself is recorded in Hindsight under tag
`mmd-pdf` + `live-verified`.
+74
View File
@@ -0,0 +1,74 @@
# MMD PDF — product requirements
| | |
|---|---|
| Version | 0.1 |
| Date | 2026-08-27 |
| Sign-off | **unsigned** |
| Repo | `gitea.baobab-ts.com/mmd-it/mmd-pdf` |
## What it is
A PDF editor for MMD staff on Windows. Free, open source, no subscription, no
account, nothing sent anywhere. It is a GPL-3.0 fork of KillerPDF 1.7.5 and
stays GPL-3.0; `FORK-NOTICE.md` records what MMD changed.
## Why it exists
Staff need to write inside PDF documents — fill a delivery note, correct a
figure, sign a form, mark up a drawing, turn a scan into searchable text.
Until now there was no sanctioned tool. PDFgear was evaluated on 2026-07-20 and
blocked: it forged the Windows default-PDF registry setting behind the consent
screen, confirmed by its own paid auditor.
## Who uses it
- **Plant and office staff** across Dakar, Bamako, Kigali, Luanda, Conakry,
Ouagadougou and Dubai. Not computer people. Many read French, not English.
- **Their screens are 800×600.** Every surface must work at that size. This is
a hard constraint, not a target.
## In scope
- Read a PDF, and change text that is already on the page.
- Fill and sign forms, including comb fields, checkboxes and radio groups.
- Mark up: highlight, draw, shapes, stamps, images, signatures.
- Page work: merge, extract, delete, reorder, crop, rotate.
- Turn a scan into searchable text (on-device, eleven languages).
- Pull pages straight off a network copier. **Not built.**
- Print.
## Not in scope
- Anything that leaves the machine. No cloud, no account, no telemetry.
- Collaborative editing.
- Creating documents from scratch — that is Word's job.
## Hard requirements
1. **An edited figure must be gone from the file, not covered over.** Upstream
painted a white box over the original text and drew the replacement on top;
extracting text returned both values. A corrected quantity on a delivery
note that leaves the building must not carry the old number inside it.
2. **800×600.** See above.
3. **The MMD shell design system** ("red plate"), light only, official MMD
lockup, red reserved for one primary action, focus, and urgency.
4. **No admin rights needed** to install or run.
5. **French and English** interface at minimum; the upstream base ships twelve
locales.
## Data
The user's own files on their own machine. Nothing else. No network calls
except the optional download of a higher-accuracy text-recognition language
pack, and the scanner on the local network once that is built.
## Open questions
- No official square MMD app mark exists. The icon crops the lockup's baobab
disc; it reads from 32px and is muddy at 1624px.
- Which of three toolbar treatments (see `SCREENS.md`) — the independent
reviewer recommends C.
- Code signing. Re-signing under an MMD certificate resets the reputation the
upstream signed build has, and SmartScreen is set to block without an
override on the pilot group.
+118
View File
@@ -0,0 +1,118 @@
# MMD PDF — screens
Every screen is 800×600. Budget: 28px title bar + 2023px status bar leaves
about 550px for the toolbar and the page.
Design source files: `design/*.dc.html`, generated by `design/build.py` on the
red-plate stylesheet `design/redplate.css` (lifted verbatim from the MMD shell
design system, not retyped). Renders: `design/shots/*.png`.
**These are mockups. The design failed independent review on 2026-08-27 and is
not approved. Do not build from them yet.**
---
## Directions — the toolbar decision
Not a screen; a comparison sheet. A PDF editor carries about thirty tools and
thirty named tools need roughly 1400px of row. Three ways out:
```
+------------------------------------------------------------------+
| A - one row, named tools |
| [Open][Save][Print][Scan][OCR] | [Select][Text][Mark]... [Fit][…]|
| 40px chrome · 512px page · 16 of 30 tools behind the "…" |
+------------------------------------------------------------------+
| B - icons only, nothing hidden |
| [][][][][][][][][][][][][][][][][][][][][] [Fit - +] |
| 30px chrome · 522px page · nothing hidden, everything unlabelled |
+------------------------------------------------------------------+
| C - three tabs, named tools, nothing hidden |
| DOCUMENT | EDIT | PAGES |
| [Select][Text][Mark][Draw][Shape][Sign][Stamp][Image] [Fit - +] |
| 56px chrome · 496px page · nothing hidden, one click deeper |
+------------------------------------------------------------------+
```
Independent reviewer's recommendation: **C**. A is the one drawn on every other
screen and it hides sixteen tools behind an unlabelled "…" button; B asks a
first-time clerk to guess thirty glyphs.
---
## Main — a document open
```
+------------------------------------------------------------------+
| [MMD] MMD PDF / BON-2026-0847 delivery note.pdf - [] X | 28px
+------------------------------------------------------------------+
| [Open][Save][Print][Scan][OCR] | [Select][Text][Mark][Draw] | 40px
| [Shape][Sign] | [Pages][Search][Undo] [FIT WIDTH - +] […] |
+---+--------------------------------------------------------------+
| > | |
| 1 | +--------------------------------------+ |
| | | MMD STEEL GROUP - DELIVERY NOTE | |
| | | | |
| | | Supplier: Fabrimetal SARL | |
| | | Reference: BON-2026-0847 | |
| | | Quantity delivered: 12 tonnes | |
| | +--------------------------------------+ |
+---+--------------------------------------------------------------+
| Ready 1 OF 1 · FIT WIDTH · 100% | 23px
+------------------------------------------------------------------+
```
- **Title bar** — official MMD lockup at 13px, then the application name in
Fraunces, then the file name. Window controls right.
- **Tool strip** — grouped: document actions, then editing tools, then
navigation. The armed tool is a solid red fill with white glyph; that is the
only red on the screen.
- **Closed page strip** (2426px) — a chevron to open the page list and the
page you are on. It reclaims 62px over the open rail, which matters at 800px.
- **Page** — white on the F3F3F0 field, hairline border, no shadow.
- **Status bar** — what the application is doing, left; where you are, right.
## EditText — changing a figure on the page
Same as Main plus a **context bar** (28px) that exists only while the Text tool
is armed: colour swatches, size, bold/italic, and the sentence
"Enter keeps it · Esc throws it away" — both were guessed wrong in testing.
The edited run is boxed in red on a red wash. The status bar turns red and
says **"The old quantity is removed from the file when you save."** That is the
moment the original is destroyed and it must be unmissable.
## PageRail — the page list open
The 88px rail replaces the closed strip: `PAGES / MARKS` headers, then one
thumbnail per page. The page you are on carries a 3px red left edge.
## Nothing — no document open
The editing tools are dimmed to the label tone. The work area becomes a dense
table of recent documents: reference, document, pages, state. Row click opens.
Two actions in the section header: `Open a file…` outlined, `Scan a document`
the one red fill.
## Save — writing over the original
The application stays visible and dimmed behind. The dialog is white, ink-ruled
title, and says in plain words what changes:
```
Save
Write the change into BON-2026-0847 delivery note.pdf?
WAS Quantity delivered: 12 tonnes
BECOMES Quantity delivered: 19 tonnes
THE OLD TEXT is taken out of the file, not covered over
[ Cancel ] [ Keep both ] [ Replace ]
```
`Replace` is the one red fill — the destructive action is correctly primary
because it is what the person came to do. **Open finding:** "Keep both" does not
yet say what filename it produces.
## Scan — pulling pages off a copier
**Not built.** Same dialog shape: copier, feed, colour, detail, and text-search
on by default in French and English. `Scan` is the one red fill.
+71
View File
@@ -0,0 +1,71 @@
# MMD PDF — versions and sign-off log
Only Ben signs. An agent's own screenshots, a green build, or an independent
reviewer's PASS are all evidence — none of them is a signature.
## Log
| Date | Feature | Ver | Event | Who | Evidence |
|---|---|---|---|---|---|
| 2026-08-27 | PDF-01…06 | 1.7.5 / 0.1 | proven end-to-end, keyboard and mouse only, on mmd-win-test-01 | kua-agent | Hindsight `mmd-pdf` + `live-verified` |
| 2026-08-27 | PDF-03 | 0.1 | `12 tonnes` no longer recoverable from the saved file; `19 tonnes` present; other lines intact | kua-agent | `pdftotext` before/after on the VM |
| 2026-08-27 | PDF-06 | 1.7.5 | `.pdf` UserChoice ProgId + Hash unchanged; deny ACE intact; no helper binary | kua-agent | registry read before and after install and first open |
| 2026-08-27 | PDF-07…09 | 0.1 | **DESIGN-FAIL** — independent adversarial review, 3 blockers, 5 major, 3 minor | independent reviewer (separate agent, default-FAIL) | findings below |
| 2026-08-27 | — | — | **awaiting Ben** | — | — |
## The design gate — OPEN
### DESIGN-FAIL, 2026-08-27, round 1
**Blockers**
1. The `···` overflow control is an unlabelled icon-only button and the only
way to sixteen of thirty tools. Icon-only is permitted for window controls
only. → label it, or ship Direction C which hides nothing.
2. Every toolbar tool name is 7.5px — below the reading floor, on the one screen
size that matters, for the exact text the design's argument rests on.
→ 910px minimum; if fourteen names then stop fitting, that is the argument
for C over A.
3. Disabled tools use `#A9A69C` — off-palette and lighter than the `#6B6A64`
floor. → `#767368`.
**Major**
4. Eleven off-palette values across the set. *(Partly my bad brief: `#FAFAF8`
and `#C9C6BC` are legitimate design-system tokens I failed to give the
reviewer. `#C9C7C0` on the page thumbnails is genuinely mine and wrong.)*
5. The whole mono-label scale runs 7.58.5px, including the data-loss warning
meant to stop a clerk destroying a figure. → 99.5px floor.
6. `OCR` is an acronym sitting among plain words. → "Read text".
7. Direction A hides sixteen tools; B asks a first-timer to guess thirty
glyphs. **Reviewer picks C.** → rebuild the four screens on C.
8. The recent-documents table leaves a dead band above the status bar.
**Minor**
9. Page thumbnails show generic grey bars, not the real page.
10. The toolbar's first icon and the status bar's first text do not share a
left column (~1417px vs ~910px).
11. "Keep both" does not say what filename it creates.
**What the reviewer said works:** the Save and Scan dialogs each keep exactly
one red fill for the one primary action, with plain button words and the
destructive action correctly primary; Save states the consequence in plain
language; the recent table matches the table spec exactly; square corners, no
gradients or grain, Fraunces for titles and mono for data throughout.
### Fix state at handover
Applied: findings 2, 3, 5 (type scale to a 9.5px floor; disabled tone to
`#767368`), and the `#C9C7C0` half of 4. Also fixed before the review landed:
two competing red fills per dialog, dialogs floating on an empty field, the
selection edge painting down every cell, and the stacked "1 / 1".
Reverted deliberately: I had edited `redplate.css`, the design system's own
stylesheet, to chase finding 4. That file is another session's and is lifted
verbatim — it has been restored from source. `#FAFAF8` and `#C9C6BC` stay.
Not applied: **1, 6, 7, 8, 9, 10, 11** — including the rebuild onto Direction C,
which is the big one and needs Ben's pick first.
**No re-review has run.** The design is DESIGN-FAIL as it stands.
+148
View File
@@ -0,0 +1,148 @@
# MMD PDF — test cases, test data, agent tasking
## The rig
Testing happens on **mmd-win-test-01** — VM 203 on hypervisor mmd-fsn-03
(Falkenstein, 168.119.88.172 / tailnet 100.64.0.19), Windows Server 2022,
guest 10.10.20.22. It auto-starts with the host.
**A user test uses keyboard and mouse only.** The rig is
`/opt/mmd-wintest/wintest` on the hypervisor — Xvfb + xfreerdp3 + xdotool, so
every input is a real pointer or key event. There is no scripting back door in
that path.
```
wintest start | stop | status | shot <name>
wintest move <x> <y> · click [1|2|3] · dblclick · at <x> <y> [btn]
wintest drag <x1> <y1> <x2> <y2> · scroll up|down [n]
wintest type "<text>" · key <keys...>
```
`/root/winrun.py` (PowerShell over WinRM) exists for **setup and for reading a
result back** — installing, copying a file in, extracting text from a saved
PDF. It is never used to make something happen inside the application.
Gotcha: `Stop-Process` on a running GUI app hangs the WinRM session. Launch GUI
apps with a **new** scheduled-task name:
`schtasks /create /tn X /tr "..." /ru Administrator /it /f` then `/run`.
## Test data
`/root/kpdf/mkpdf.py` on the hypervisor writes a one-page delivery note:
```
MMD STEEL GROUP - DELIVERY NOTE
Supplier: Fabrimetal SARL
Reference: BON-2026-0847
Quantity delivered: 12 tonnes
Received by: ____________________
Editing test: change the quantity above to 14 tonnes.
```
Serve it to the VM from the hypervisor (the VM reaches 10.10.20.1, and now the
internet too after its DNS was fixed on 2026-08-27):
`cd /root/kpdf && python3 -m http.server 8899 --bind 10.10.20.1`
---
## TC-01 · Open and read a PDF — PDF-01
**Input** Launch `MmdPdf.exe` with `C:\src\clean-note.pdf` as its argument.
**Expected** Window opens; the page renders; the status bar reads `1 OF 1`; the
title bar carries the file name.
## TC-02 · Change text already on the page — PDF-02
**Input** Double-click the words "Quantity delivered: 12 tonnes". Press `End`,
nine × `BackSpace`, type `19 tonnes`, press `Enter`.
**Expected** The line reads "Quantity delivered: 19 tonnes" on screen.
## TC-03 · The old text is removed, not covered — PDF-03 · **the one that matters**
**Input** After TC-02, click Save, answer `Replace`. Then read the file back:
`pdftotext delivery-note.pdf -`
**Expected** The extracted text contains `19 tonnes` and **does not contain**
`12 tonnes`. Every other line is unchanged and still extractable.
**Falsification** Before the fix this returned both figures. If `12 tonnes`
appears, the fix has regressed and the tool must not be used to correct figures.
## TC-04 · Save asks before it overwrites — PDF-04
**Input** Edit anything, click Save.
**Expected** A dialog naming the file. Nothing is written to disk until it is
answered. `Cancel` leaves the file byte-identical.
## TC-05 · No admin rights, portable — PDF-05
**Input** Copy the single `.exe` to a folder in a standard user's profile and
run it.
**Expected** It runs. No elevation prompt. Nothing is written to Program Files.
## TC-06 · Leaves the Windows default-PDF setting alone — PDF-06 · **security**
**Input** Read `HKCU:\...\FileExts\.pdf\UserChoice` (ProgId, Hash) and the
key's ACL. Install, launch, open a PDF. Read them again.
**Expected** ProgId, Hash and the deny ACE are **identical**. No helper binary
appears beside the executable.
**Falsification** PDFgear forged this hash and stripped the deny ACE. If either
changes, the tool is blocked, as PDFgear was.
**Read-only** — this test only reads the registry.
## TC-07 · MMD name, icon and word-mark — PDF-07
**Input** Look at the title bar, the taskbar icon, the About box, and Explorer's
icon for the file type.
**Expected** MMD PDF everywhere; the official lockup in the title bar; no trace
of the upstream product name or author. Icon legible at 32px and above.
## TC-08 · MMD design system, light only — PDF-08 · ⛔ blocked
**Input** Open Settings; look for a theme or accent picker. Sample the chrome,
rail, page-field and ink colours.
**Expected** No picker exists. Values match `Themes/MMD.xaml`. No dark surface
anywhere. Windows title bar stays light.
**Blocked** until the design passes review and Ben signs it.
## TC-09 · Works at 800×600 — PDF-09 · ⛔ blocked
**Input** Set the VM to 800×600. Open a document. Reach every tool.
**Expected** No horizontal scrollbar on the chrome. Every tool reachable. Every
label a person must read is at least 9px. The page is visible without scrolling
its own width.
**Blocked** on the toolbar direction (A/B/C).
## TC-10 … TC-18 — not written
Forms, mark-up, page work, text recognition, scanning, French, print, signing
and Intune are ⚪ in the backlog. Write the case when the feature is picked up;
do not pre-write cases for work nobody is doing.
---
## Agent tasking
**To run a user test** (TC-01 … TC-09):
> You are testing MMD PDF on mmd-win-test-01. Reach the hypervisor with
> `ssh -i ~/.ssh/id_ed25519_mmd_fsn03_rescue root@100.64.0.19`. Start the rig
> with `wintest start`. **Every action inside the application must be a real
> keyboard or mouse event through `wintest`** — never PowerShell, never a
> registry write, never a command-line argument to make something happen.
> `/root/winrun.py` is allowed only to put a file on the VM, launch the app, and
> read a result back. Run <TC-nn> exactly as written. Screenshot before and
> after every step with `wintest shot`, decode each PNG and **look at it**.
> Report each step as it happened, including anything that did not work. If you
> cannot perform a step, say `blocked` and name the exact interaction — do not
> substitute a different route to the same end.
**To review a design** (any UI change):
> You are an independent design reviewer. You did not build this. Your default
> verdict is DESIGN-FAIL; only evidence moves you off it. Read the screenshots
> as images. Judge against: 800×600, light only, the fixed palette, red only for
> one primary action / focus / urgency, square corners, the type scale, a 9px
> floor on any text a person must read, no icon-only buttons except the window
> controls, dense but not cramped, and whether a plant clerk who is not a
> computer person could use it. Output a verdict line then numbered findings,
> each with severity, the screen, the specific element, and the concrete fix.
> DESIGN-PASS only with zero blockers and zero majors.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+224
View File
@@ -0,0 +1,224 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
<style>
body{margin:0;background:#F3F3F0;font-family:"PP Neue Machina","Neue Machina",system-ui,-apple-system,"Segoe UI",sans-serif;color:#3C3A34;-webkit-font-smoothing:antialiased}
a{color:#C8102E} a:hover{color:#8E0B20}
*{box-sizing:border-box}
.root{padding:16px;background:#F3F3F0}
.shell{width:1180px;display:flex;flex-direction:column;font-size:11.5px;background:#FFFFFF;border:1px solid #C7C6BD}
.s-title{display:flex;align-items:center;gap:10px;padding:0 10px;height:30px;background:#C8102E;color:#fff;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px}
.s-title .mmd-logo{height:19px;width:auto;display:block;flex:none;fill:#fff}
.spring{flex:1}
.s-who{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.07em;text-transform:uppercase;color:rgba(255,255,255,0.85)}
.s-signout{font-size:10px;padding:1px 0;color:#fff;text-decoration:underline;text-underline-offset:2px}
.s-bar{display:flex;align-items:stretch;gap:2px;padding:0 5px;background:#F7F7F3;border-bottom:1px solid #D7D6CE}
.s-item{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;white-space:nowrap;border:1px solid transparent;color:#3C3A34}
.s-item .cv{font-size:8px;opacity:0.5}
.s-item .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;padding:0 4px;border:1px solid #23221E;color:#23221E;background:#FFFFFF;line-height:13px}
.s-item.on{background:#FFFFFF;border-color:#D7D6CE;border-top:2px solid #C8102E;border-bottom-color:#FFFFFF;margin-bottom:-1px;font-weight:700;color:#23221E}
.s-item.on .n{border-color:#23221E;color:#23221E}
.s-tools{margin-left:auto;display:flex;align-items:center;gap:6px;padding-right:4px}
.s-search{width:168px;padding:2px 7px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
.s-btn{padding:2px 9px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
.s-desk{position:relative;background:#D4D0C6;overflow:hidden}
.s-desk.tiled{display:grid;grid-template-columns:1.35fr 1fr;grid-template-rows:auto auto;gap:10px;padding:10px;align-items:start}
.s-desk.stacked{height:492px}
.s-desk.oneup{padding:10px}
.s-task{display:flex;align-items:center;gap:10px;padding:4px 8px;font-size:10.5px;background:#F1F1EC;border-top:1px solid #D7D6CE}
.s-task .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.s-group{display:inline-flex;align-items:stretch;border:1px solid #D7D6CE;background:#FFFFFF}
.s-group .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:2px 7px;border-right:1px solid #D7D6CE;display:flex;align-items:center;background:#F7F7F3}
.s-group .win{padding:2px 8px;font-size:10px;color:#23221E;border-right:1px solid #D7D6CE;display:flex;align-items:center;gap:5px;background:#F1F1EC}
.s-group .win:last-child{border-right:0}
.s-group .win.on{box-shadow:inset 3px 0 0 #C8102E;font-weight:700;background:#FFFFFF}
.s-group .win.min{color:#3C3A34;background:#FFFFFF;outline:1px dashed #6B6A64;outline-offset:-3px}
.s-task .tile{font-size:10px;padding:1px 8px;border:1px solid #C7C6BD;background:#FFFFFF;color:#3C3A34}
/* windows — title bar made visible: darker plate, ink name, clear border; front = red top edge + shadow */
.win-frame{background:#FFFFFF;border:1px solid #B9B8AE;display:flex;flex-direction:column;min-width:0;box-shadow:0 1px 2px rgba(35,34,30,0.12)}
.win-frame.front{border-color:#767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
.stacked .win-frame{position:absolute}
.w-title{display:flex;align-items:center;gap:8px;height:28px;padding:0 6px 0 8px;background:#E4E3DC;border-bottom:1px solid #C9C6BC;font-size:11px;color:#3C3A34}
.front .w-title{background:#FFFFFF;color:#23221E;border-top:3px solid #C8102E;height:31px}
.w-title .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#23221E;font-weight:700}
.w-title .app::after{content:"/";margin-left:8px;color:#6B6A64}
.w-title .name{font-family:Fraunces,Georgia,serif;font-weight:600;font-size:12px;color:#3C3A34}
.front .w-title .name{color:#23221E;font-weight:700}
.w-title .new{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#23221E;background:#E4E3DC;padding:0 6px;line-height:15px}
.w-title .ctl{display:flex;gap:8px}
.w-title .ctl span{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#23221E;background:transparent;border:0}
.w-body{padding:0;overflow:hidden}
.w-toolbar{display:flex;align-items:center;gap:8px;padding:5px 8px;background:#FAFAF8;border-bottom:1px solid #E6E5DF}
.w-h1{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:13px;letter-spacing:-0.01em;margin:0;color:#23221E}
.w-count{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#6B6A64}
.w-count b{color:#C8102E;font-weight:600}
.w-filters{margin-left:auto;display:flex;gap:4px}
.w-chip{padding:1px 7px;font-size:9.5px;font-family:"JetBrains Mono",ui-monospace,monospace;border:1px solid #D7D6CE;background:#FFFFFF;color:#3C3A34}
.w-chip.on{border-color:#767368;color:#23221E;font-weight:600}
.w-cta{padding:2px 9px;font-size:10px;background:#C8102E;color:#FFFFFF;border:1px solid #C8102E;font-weight:600}
table.w-tbl{width:100%;border-collapse:collapse;font-size:10.5px;background:#FFFFFF}
.w-tbl th{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;text-align:left;padding:4px 8px;white-space:nowrap;background:#F1F1EC;color:#23221E;border-bottom:1px solid #D7D6CE;border-right:1px solid #E6E5DF}
.w-tbl td{padding:4px 8px;border-bottom:1px solid #E6E5DF;border-right:1px solid #E6E5DF;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
.w-tbl td:last-child,.w-tbl th:last-child{border-right:0}
.w-tbl tr:nth-child(even) td{background:#FAFAF8}
.w-tbl td.num{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;text-align:right}
.w-tbl th.num{text-align:right}
.w-tbl td.ref{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.03em}
.w-tbl tr.sel td{background:#F7F7F3;box-shadow:inset 3px 0 0 #C8102E}
.w-state{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.07em;text-transform:uppercase;padding:1px 5px;background:#F1F1EC;color:#23221E}
.rec{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:0}
.rec .col{padding:6px 10px 8px;border-right:1px solid #E6E5DF}
.rec .col:last-child{border-right:0}
.rec h4{margin:4px 0 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;font-weight:600;border-bottom:1px solid #E6E5DF;padding-bottom:3px}
.kv{display:grid;grid-template-columns:96px 1fr;gap:2px 8px;font-size:10.5px;margin:0}
.kv dt{color:#767368}
.kv dd{margin:0;color:#23221E}
.kv dd.mono{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;font-variant-numeric:tabular-nums}
.rec-actions{display:flex;gap:6px;padding:6px 10px;border-top:1px solid #E6E5DF;background:#FAFAF8}
.rec-actions span{padding:2px 9px;font-size:10px;border:1px solid #C7C6BD;background:#FFFFFF;color:#23221E}
.rec-actions span.primary{color:#FFFFFF;background:#C8102E;border:1px solid #C8102E;font-weight:600}
.steps{display:flex;gap:0;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;border-bottom:1px solid #E6E5DF}
.steps span{padding:3px 8px;border-right:1px solid #E6E5DF;color:#6B6A64}
.steps span.done{color:#767368}
.steps span.now{color:#C8102E;font-weight:700;box-shadow:inset 0 -2px 0 #C8102E}
.rep{padding:6px 10px}
.rep .row{display:grid;grid-template-columns:150px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid #E6E5DF;font-size:10.5px}
.rep .row:last-child{border-bottom:0}
.rep .name{color:#23221E;font-weight:600}
.rep .name small{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.08em;text-transform:uppercase;color:#6B6A64;font-weight:400}
.rep .hl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;font-variant-numeric:tabular-nums}
/* title-bar option closeups */
.opt{width:600px;padding:16px;background:#D4D0C6}
.opt .cap{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;margin:0 0 8px}
.opt .win-frame{margin-bottom:14px}
.opt .win-frame:last-child{margin-bottom:0}
/* Option B: ink strip */
.optB .front .w-title{background:#3C3A34;color:#fff;border-top:0;height:28px;border-bottom:1px solid #23221E}
.optB .front .w-title .name{color:#fff}
.optB .front .w-title .app{color:#fff}
.optB .front .w-title .app::after{color:rgba(255,255,255,0.35)}
.optB .front .w-title .ctl span{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.6)}
.optB .front .w-title .new{color:#fff;border-color:rgba(255,255,255,0.7)}
/* Option C: ruled */
.optC .w-title{background:#FFFFFF;border-bottom:2px solid #23221E;height:28px}
.optC .front .w-title{background:#FFFFFF;border-top:3px solid #C8102E;height:31px;border-bottom:2px solid #23221E}
.optC .win-frame{border-color:#23221E}
/* ---- MMD PDF: the parts the shell system has no component for yet ------
Named for adoption into the design system, not invented per screen.
Every colour below is a red-plate value; nothing new is introduced. */
.pdf{width:800px;height:600px;display:flex;flex-direction:column;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26);overflow:hidden}
/* tool strip */
.pdf-tools{display:flex;align-items:stretch;background:#F1F1EC;border-bottom:1px solid #D7D6CE}
.pdf-t{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:44px;color:#23221E;flex:none}
.pdf-t .lb{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:7.5px;letter-spacing:0.02em;text-transform:uppercase;line-height:1;white-space:nowrap}
.pdf-t.on{background:#C8102E;color:#FFFFFF}
.pdf-t.dim{color:#A9A69C}
.pdf-rule{width:1px;background:#D7D6CE;margin:6px 3px;flex:none}
.pdf-end{margin-left:auto;display:flex;align-items:center;gap:5px;padding:0 6px;flex:none}
.pdf-zoom{display:flex;align-items:center;border:1px solid #C7C6BD;background:#FFFFFF}
.pdf-zoom b{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;font-weight:500;color:#23221E;padding:0 7px;line-height:19px}
.pdf-zoom i{width:19px;line-height:19px;text-align:center;font-style:normal;font-size:11px;color:#23221E;border-left:1px solid #C7C6BD}
.pdf-over{width:26px;display:flex;align-items:center;justify-content:center;color:#23221E;border-left:1px solid #D7D6CE;flex:none}
/* icon-only strip (direction B) */
.pdf-tools--icons .pdf-t{width:28px}
.pdf-tools--icons .pdf-t .lb{display:none}
/* tab strip (direction C) */
.pdf-tabs{display:flex;align-items:stretch;background:#E4E3DC;border-bottom:1px solid #D7D6CE;padding:0 4px}
.pdf-tab{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:0 11px;line-height:21px}
.pdf-tab.on{background:#FFFFFF;color:#23221E;font-weight:600;border-top:2px solid #C8102E;line-height:19px}
/* context bar — only present while a tool is armed */
.pdf-ctx{display:flex;align-items:center;gap:9px;height:26px;padding:0 9px;background:#FAFAF8;border-bottom:1px solid #E6E5DF;font-size:10px;color:#3C3A34}
.pdf-ctx .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.pdf-sw{display:flex;gap:3px}
.pdf-sw i{width:12px;height:12px;display:block;border:1px solid #C7C6BD}
.pdf-sw i.on{outline:1px solid #23221E;outline-offset:1px}
.pdf-pick{border:1px solid #C7C6BD;background:#FFFFFF;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;padding:1px 6px}
/* work area */
.pdf-work{flex:1;display:flex;min-height:0}
.pdf-strip{width:26px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:7px}
.pdf-strip .pn{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;color:#23221E;line-height:1.25;text-align:center}
.pdf-rail{width:88px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;overflow:hidden}
.pdf-rail-h{display:flex;gap:7px;padding:5px 7px 3px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.pdf-rail-h .on{color:#23221E;font-weight:700}
.pdf-pg{margin:0 7px 5px;background:#FFFFFF;border:1px solid #C7C6BD}
.pdf-pg.here{box-shadow:inset 3px 0 0 #C8102E}
.pdf-pg .mini{height:64px;padding:5px 6px;overflow:hidden}
.pdf-pg .mini i{display:block;background:#C9C7C0;height:2px;margin-bottom:3px}
.pdf-pg .mini i.t{background:#23221E;height:3px;width:74%;margin-bottom:5px}
.pdf-pg .mini i.s{width:52%}
.pdf-pg .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;text-align:center;color:#3C3A34;line-height:14px;border-top:1px solid #E6E5DF}
.pdf-canvas{flex:1;background:#F3F3F0;display:flex;justify-content:center;overflow:hidden;padding:9px 0 0}
.pdf-page{width:100%;max-width:560px;background:#FFFFFF;border:1px solid #C7C6BD;padding:28px 34px}
/* status */
.pdf-status{display:flex;align-items:center;gap:12px;height:20px;padding:0 8px;background:#F1F1EC;border-top:1px solid #D7D6CE;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;color:#3C3A34;flex:none}
.pdf-status .sep{width:1px;height:11px;background:#C7C6BD}
.pdf-status .r{margin-left:auto;color:#6B6A64}
.pdf-status .warn{color:#C8102E}
/* document body */
.pdf-h{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:14px;color:#23221E;margin:0 0 13px}
.pdf-p{font-size:10.5px;color:#3C3A34;line-height:1.9;margin:0}
.pdf-live{display:inline-block;padding:0 4px;border:1px solid #C8102E;background:#FBEEF0;color:#23221E}
/* dialog */
.pdf-behind{position:absolute;inset:0}
.pdf-behind::after{content:"";position:absolute;inset:0;background:rgba(35,34,30,0.28)}
.pdf-over-scrim{position:relative;width:800px;height:600px;display:flex;align-items:center;justify-content:center;overflow:hidden}
.pdf-over-scrim .pdf-dlg{position:relative;z-index:2}
.pdf-dlg{width:404px;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
.pdf-dlg-t{display:flex;align-items:center;height:26px;padding:0 10px;background:#FFFFFF;color:#23221E;border-bottom:2px solid #23221E;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:11.5px}
.pdf-dlg-b{padding:13px 15px;display:flex;flex-direction:column;gap:10px;font-size:10.5px;color:#3C3A34}
.pdf-dlg-a{display:flex;justify-content:flex-end;gap:6px;padding:0 15px 13px}
.pdf-kv{display:grid;grid-template-columns:82px 1fr;gap:4px 10px;align-items:baseline}
.pdf-kv .k{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.09em;text-transform:uppercase;color:#767368}
.pdf-in{border:1px solid #C7C6BD;background:#FFFFFF;padding:3px 7px;font-size:10.5px;color:#23221E;display:flex;align-items:center}
.pdf-in .cv{margin-left:auto;font-size:8px;color:#767368}
/* direction board */
.board{width:800px;background:#F3F3F0;padding:11px;display:flex;flex-direction:column;gap:11px}
.board h3{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px;color:#23221E;margin:0}
.board .why{font-size:10px;color:#3C3A34;margin:0}
.board .cost{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;color:#767368;margin:0}
.board .card{background:#FFFFFF;border:1px solid #C7C6BD;padding:9px 10px;display:flex;flex-direction:column;gap:6px}
.board .strip{border:1px solid #D7D6CE}
</style>
</helmet>
<div class="board">
<div class="card">
<h3>A &mdash; one row, named tools</h3>
<div class="strip"><div class="pdf-tools" style="height:40px"><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M2 6h6l2 2h8v9H2z"/></svg><span class="lb">Open</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3"/><path d="M6 17v-5h8v5"/></svg><span class="lb">Save</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 8V3h8v5"/><path d="M3 8h14v6h-3v4H6v-4H3z"/></svg><span class="lb">Print</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5V3h4"/><path d="M17 5V3h-4"/><path d="M3 15v2h4"/><path d="M17 15v2h-4"/><path d="M2 10h16"/></svg><span class="lb">Scan</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 6V3h3"/><path d="M17 6V3h-3"/><path d="M3 14v3h3"/><path d="M17 14v3h-3"/><path d="M6 9h8"/><path d="M6 12h5"/></svg><span class="lb">OCR</span></div><span class="pdf-rule"></span><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg><span class="lb">Select</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg><span class="lb">Text</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg><span class="lb">Mark</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg><span class="lb">Draw</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg><span class="lb">Shape</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg><span class="lb">Sign</span></div><span class="pdf-rule"></span><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 3h8v14H4z"/><path d="M12 6h4v11H8"/></svg><span class="lb">Pages</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 13l4 4"/><path d="M4 9a5 5 0 1010 0 5 5 0 10-10 0"/></svg><span class="lb">Search</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M7 6L3 10l4 4"/><path d="M3 10h9a4 4 0 010 8"/></svg><span class="lb">Undo</span></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>&minus;</i><i>+</i></span></div><div class="pdf-over"><svg width="14" height="14" viewBox="0 0 14 14" fill="#23221E"><circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/><circle cx="11" cy="7" r="1.2"/></svg></div></div></div>
<p class="why">Fourteen tools carry their name. Everything else &mdash; stamp, image, crop, merge, flatten, redo &mdash; lives behind the three dots on the right.</p>
<p class="cost">40px of chrome &middot; 512px left for the page &middot; 16 of 30 tools hidden</p>
</div>
<div class="card">
<h3>B &mdash; icons only, nothing hidden</h3>
<div class="strip"><div class="pdf-tools pdf-tools--icons" style="height:30px"><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M2 6h6l2 2h8v9H2z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3"/><path d="M6 17v-5h8v5"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 8V3h8v5"/><path d="M3 8h14v6h-3v4H6v-4H3z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5V3h4"/><path d="M17 5V3h-4"/><path d="M3 15v2h4"/><path d="M17 15v2h-4"/><path d="M2 10h16"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 6V3h3"/><path d="M17 6V3h-3"/><path d="M3 14v3h3"/><path d="M17 14v3h-3"/><path d="M6 9h8"/><path d="M6 12h5"/></svg></div><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 16h8"/><path d="M7 13h6l-1-5a2 2 0 10-4 0z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 4h14v12H3z"/><path d="M3 13l4-4 3 3 3-3 4 4"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 2v12h12"/><path d="M2 6h12v12"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5h6v4H3z"/><path d="M11 11h6v4h-6z"/><path d="M6 9v3h5"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 3h8v7H6z"/><path d="M3 13h14"/><path d="M3 16h14"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 3h8v14H4z"/><path d="M12 6h4v11H8"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 13l4 4"/><path d="M4 9a5 5 0 1010 0 5 5 0 10-10 0"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M7 6L3 10l4 4"/><path d="M3 10h9a4 4 0 010 8"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 6l4 4-4 4"/><path d="M17 10H8a4 4 0 000 8"/></svg></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>&minus;</i><i>+</i></span></div></div></div>
<p class="why">All thirty fit, name on hover. Fastest once someone knows the icons; a plant clerk opening it for the first time has to guess.</p>
<p class="cost">30px of chrome &middot; 522px left for the page &middot; nothing hidden</p>
</div>
<div class="card">
<h3>C &mdash; three tabs, named tools, nothing hidden</h3>
<div class="strip"><div class="pdf-tabs"><span class="pdf-tab">Document</span><span class="pdf-tab on">Edit</span><span class="pdf-tab">Pages</span></div><div class="pdf-tools" style="height:34px"><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg><span class="lb">Select</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg><span class="lb">Text</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg><span class="lb">Mark</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg><span class="lb">Draw</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg><span class="lb">Shape</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg><span class="lb">Sign</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 16h8"/><path d="M7 13h6l-1-5a2 2 0 10-4 0z"/></svg><span class="lb">Stamp</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 4h14v12H3z"/><path d="M3 13l4-4 3 3 3-3 4 4"/></svg><span class="lb">Image</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 2v12h12"/><path d="M2 6h12v12"/></svg><span class="lb">Crop</span></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>&minus;</i><i>+</i></span></div></div></div>
<p class="why">Document, Edit, Pages. Every tool keeps its name and every tool is reachable, at the price of one click to change tab and a second bar of chrome.</p>
<p class="cost">56px of chrome &middot; 496px left for the page &middot; nothing hidden, one click deeper</p>
</div>
</div></x-dc>
</body>
</html>
File diff suppressed because one or more lines are too long
+205
View File
@@ -0,0 +1,205 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
<style>
body{margin:0;background:#F3F3F0;font-family:"PP Neue Machina","Neue Machina",system-ui,-apple-system,"Segoe UI",sans-serif;color:#3C3A34;-webkit-font-smoothing:antialiased}
a{color:#C8102E} a:hover{color:#8E0B20}
*{box-sizing:border-box}
.root{padding:16px;background:#F3F3F0}
.shell{width:1180px;display:flex;flex-direction:column;font-size:11.5px;background:#FFFFFF;border:1px solid #C7C6BD}
.s-title{display:flex;align-items:center;gap:10px;padding:0 10px;height:30px;background:#C8102E;color:#fff;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px}
.s-title .mmd-logo{height:19px;width:auto;display:block;flex:none;fill:#fff}
.spring{flex:1}
.s-who{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.07em;text-transform:uppercase;color:rgba(255,255,255,0.85)}
.s-signout{font-size:10px;padding:1px 0;color:#fff;text-decoration:underline;text-underline-offset:2px}
.s-bar{display:flex;align-items:stretch;gap:2px;padding:0 5px;background:#F7F7F3;border-bottom:1px solid #D7D6CE}
.s-item{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;white-space:nowrap;border:1px solid transparent;color:#3C3A34}
.s-item .cv{font-size:8px;opacity:0.5}
.s-item .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;padding:0 4px;border:1px solid #23221E;color:#23221E;background:#FFFFFF;line-height:13px}
.s-item.on{background:#FFFFFF;border-color:#D7D6CE;border-top:2px solid #C8102E;border-bottom-color:#FFFFFF;margin-bottom:-1px;font-weight:700;color:#23221E}
.s-item.on .n{border-color:#23221E;color:#23221E}
.s-tools{margin-left:auto;display:flex;align-items:center;gap:6px;padding-right:4px}
.s-search{width:168px;padding:2px 7px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
.s-btn{padding:2px 9px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
.s-desk{position:relative;background:#D4D0C6;overflow:hidden}
.s-desk.tiled{display:grid;grid-template-columns:1.35fr 1fr;grid-template-rows:auto auto;gap:10px;padding:10px;align-items:start}
.s-desk.stacked{height:492px}
.s-desk.oneup{padding:10px}
.s-task{display:flex;align-items:center;gap:10px;padding:4px 8px;font-size:10.5px;background:#F1F1EC;border-top:1px solid #D7D6CE}
.s-task .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.s-group{display:inline-flex;align-items:stretch;border:1px solid #D7D6CE;background:#FFFFFF}
.s-group .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:2px 7px;border-right:1px solid #D7D6CE;display:flex;align-items:center;background:#F7F7F3}
.s-group .win{padding:2px 8px;font-size:10px;color:#23221E;border-right:1px solid #D7D6CE;display:flex;align-items:center;gap:5px;background:#F1F1EC}
.s-group .win:last-child{border-right:0}
.s-group .win.on{box-shadow:inset 3px 0 0 #C8102E;font-weight:700;background:#FFFFFF}
.s-group .win.min{color:#3C3A34;background:#FFFFFF;outline:1px dashed #6B6A64;outline-offset:-3px}
.s-task .tile{font-size:10px;padding:1px 8px;border:1px solid #C7C6BD;background:#FFFFFF;color:#3C3A34}
/* windows — title bar made visible: darker plate, ink name, clear border; front = red top edge + shadow */
.win-frame{background:#FFFFFF;border:1px solid #B9B8AE;display:flex;flex-direction:column;min-width:0;box-shadow:0 1px 2px rgba(35,34,30,0.12)}
.win-frame.front{border-color:#767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
.stacked .win-frame{position:absolute}
.w-title{display:flex;align-items:center;gap:8px;height:28px;padding:0 6px 0 8px;background:#E4E3DC;border-bottom:1px solid #C9C6BC;font-size:11px;color:#3C3A34}
.front .w-title{background:#FFFFFF;color:#23221E;border-top:3px solid #C8102E;height:31px}
.w-title .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#23221E;font-weight:700}
.w-title .app::after{content:"/";margin-left:8px;color:#6B6A64}
.w-title .name{font-family:Fraunces,Georgia,serif;font-weight:600;font-size:12px;color:#3C3A34}
.front .w-title .name{color:#23221E;font-weight:700}
.w-title .new{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#23221E;background:#E4E3DC;padding:0 6px;line-height:15px}
.w-title .ctl{display:flex;gap:8px}
.w-title .ctl span{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#23221E;background:transparent;border:0}
.w-body{padding:0;overflow:hidden}
.w-toolbar{display:flex;align-items:center;gap:8px;padding:5px 8px;background:#FAFAF8;border-bottom:1px solid #E6E5DF}
.w-h1{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:13px;letter-spacing:-0.01em;margin:0;color:#23221E}
.w-count{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#6B6A64}
.w-count b{color:#C8102E;font-weight:600}
.w-filters{margin-left:auto;display:flex;gap:4px}
.w-chip{padding:1px 7px;font-size:9.5px;font-family:"JetBrains Mono",ui-monospace,monospace;border:1px solid #D7D6CE;background:#FFFFFF;color:#3C3A34}
.w-chip.on{border-color:#767368;color:#23221E;font-weight:600}
.w-cta{padding:2px 9px;font-size:10px;background:#C8102E;color:#FFFFFF;border:1px solid #C8102E;font-weight:600}
table.w-tbl{width:100%;border-collapse:collapse;font-size:10.5px;background:#FFFFFF}
.w-tbl th{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;text-align:left;padding:4px 8px;white-space:nowrap;background:#F1F1EC;color:#23221E;border-bottom:1px solid #D7D6CE;border-right:1px solid #E6E5DF}
.w-tbl td{padding:4px 8px;border-bottom:1px solid #E6E5DF;border-right:1px solid #E6E5DF;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
.w-tbl td:last-child,.w-tbl th:last-child{border-right:0}
.w-tbl tr:nth-child(even) td{background:#FAFAF8}
.w-tbl td.num{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;text-align:right}
.w-tbl th.num{text-align:right}
.w-tbl td.ref{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.03em}
.w-tbl tr.sel td{background:#F7F7F3;box-shadow:inset 3px 0 0 #C8102E}
.w-state{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.07em;text-transform:uppercase;padding:1px 5px;background:#F1F1EC;color:#23221E}
.rec{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:0}
.rec .col{padding:6px 10px 8px;border-right:1px solid #E6E5DF}
.rec .col:last-child{border-right:0}
.rec h4{margin:4px 0 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;font-weight:600;border-bottom:1px solid #E6E5DF;padding-bottom:3px}
.kv{display:grid;grid-template-columns:96px 1fr;gap:2px 8px;font-size:10.5px;margin:0}
.kv dt{color:#767368}
.kv dd{margin:0;color:#23221E}
.kv dd.mono{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;font-variant-numeric:tabular-nums}
.rec-actions{display:flex;gap:6px;padding:6px 10px;border-top:1px solid #E6E5DF;background:#FAFAF8}
.rec-actions span{padding:2px 9px;font-size:10px;border:1px solid #C7C6BD;background:#FFFFFF;color:#23221E}
.rec-actions span.primary{color:#FFFFFF;background:#C8102E;border:1px solid #C8102E;font-weight:600}
.steps{display:flex;gap:0;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;border-bottom:1px solid #E6E5DF}
.steps span{padding:3px 8px;border-right:1px solid #E6E5DF;color:#6B6A64}
.steps span.done{color:#767368}
.steps span.now{color:#C8102E;font-weight:700;box-shadow:inset 0 -2px 0 #C8102E}
.rep{padding:6px 10px}
.rep .row{display:grid;grid-template-columns:150px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid #E6E5DF;font-size:10.5px}
.rep .row:last-child{border-bottom:0}
.rep .name{color:#23221E;font-weight:600}
.rep .name small{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.08em;text-transform:uppercase;color:#6B6A64;font-weight:400}
.rep .hl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;font-variant-numeric:tabular-nums}
/* title-bar option closeups */
.opt{width:600px;padding:16px;background:#D4D0C6}
.opt .cap{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;margin:0 0 8px}
.opt .win-frame{margin-bottom:14px}
.opt .win-frame:last-child{margin-bottom:0}
/* Option B: ink strip */
.optB .front .w-title{background:#3C3A34;color:#fff;border-top:0;height:28px;border-bottom:1px solid #23221E}
.optB .front .w-title .name{color:#fff}
.optB .front .w-title .app{color:#fff}
.optB .front .w-title .app::after{color:rgba(255,255,255,0.35)}
.optB .front .w-title .ctl span{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.6)}
.optB .front .w-title .new{color:#fff;border-color:rgba(255,255,255,0.7)}
/* Option C: ruled */
.optC .w-title{background:#FFFFFF;border-bottom:2px solid #23221E;height:28px}
.optC .front .w-title{background:#FFFFFF;border-top:3px solid #C8102E;height:31px;border-bottom:2px solid #23221E}
.optC .win-frame{border-color:#23221E}
/* ---- MMD PDF: the parts the shell system has no component for yet ------
Named for adoption into the design system, not invented per screen.
Every colour below is a red-plate value; nothing new is introduced. */
.pdf{width:800px;height:600px;display:flex;flex-direction:column;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26);overflow:hidden}
/* tool strip */
.pdf-tools{display:flex;align-items:stretch;background:#F1F1EC;border-bottom:1px solid #D7D6CE}
.pdf-t{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:44px;color:#23221E;flex:none}
.pdf-t .lb{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:7.5px;letter-spacing:0.02em;text-transform:uppercase;line-height:1;white-space:nowrap}
.pdf-t.on{background:#C8102E;color:#FFFFFF}
.pdf-t.dim{color:#A9A69C}
.pdf-rule{width:1px;background:#D7D6CE;margin:6px 3px;flex:none}
.pdf-end{margin-left:auto;display:flex;align-items:center;gap:5px;padding:0 6px;flex:none}
.pdf-zoom{display:flex;align-items:center;border:1px solid #C7C6BD;background:#FFFFFF}
.pdf-zoom b{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;font-weight:500;color:#23221E;padding:0 7px;line-height:19px}
.pdf-zoom i{width:19px;line-height:19px;text-align:center;font-style:normal;font-size:11px;color:#23221E;border-left:1px solid #C7C6BD}
.pdf-over{width:26px;display:flex;align-items:center;justify-content:center;color:#23221E;border-left:1px solid #D7D6CE;flex:none}
/* icon-only strip (direction B) */
.pdf-tools--icons .pdf-t{width:28px}
.pdf-tools--icons .pdf-t .lb{display:none}
/* tab strip (direction C) */
.pdf-tabs{display:flex;align-items:stretch;background:#E4E3DC;border-bottom:1px solid #D7D6CE;padding:0 4px}
.pdf-tab{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:0 11px;line-height:21px}
.pdf-tab.on{background:#FFFFFF;color:#23221E;font-weight:600;border-top:2px solid #C8102E;line-height:19px}
/* context bar — only present while a tool is armed */
.pdf-ctx{display:flex;align-items:center;gap:9px;height:26px;padding:0 9px;background:#FAFAF8;border-bottom:1px solid #E6E5DF;font-size:10px;color:#3C3A34}
.pdf-ctx .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.pdf-sw{display:flex;gap:3px}
.pdf-sw i{width:12px;height:12px;display:block;border:1px solid #C7C6BD}
.pdf-sw i.on{outline:1px solid #23221E;outline-offset:1px}
.pdf-pick{border:1px solid #C7C6BD;background:#FFFFFF;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;padding:1px 6px}
/* work area */
.pdf-work{flex:1;display:flex;min-height:0}
.pdf-strip{width:26px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:7px}
.pdf-strip .pn{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;color:#23221E;line-height:1.25;text-align:center}
.pdf-rail{width:88px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;overflow:hidden}
.pdf-rail-h{display:flex;gap:7px;padding:5px 7px 3px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.pdf-rail-h .on{color:#23221E;font-weight:700}
.pdf-pg{margin:0 7px 5px;background:#FFFFFF;border:1px solid #C7C6BD}
.pdf-pg.here{box-shadow:inset 3px 0 0 #C8102E}
.pdf-pg .mini{height:64px;padding:5px 6px;overflow:hidden}
.pdf-pg .mini i{display:block;background:#C9C7C0;height:2px;margin-bottom:3px}
.pdf-pg .mini i.t{background:#23221E;height:3px;width:74%;margin-bottom:5px}
.pdf-pg .mini i.s{width:52%}
.pdf-pg .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;text-align:center;color:#3C3A34;line-height:14px;border-top:1px solid #E6E5DF}
.pdf-canvas{flex:1;background:#F3F3F0;display:flex;justify-content:center;overflow:hidden;padding:9px 0 0}
.pdf-page{width:100%;max-width:560px;background:#FFFFFF;border:1px solid #C7C6BD;padding:28px 34px}
/* status */
.pdf-status{display:flex;align-items:center;gap:12px;height:20px;padding:0 8px;background:#F1F1EC;border-top:1px solid #D7D6CE;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;color:#3C3A34;flex:none}
.pdf-status .sep{width:1px;height:11px;background:#C7C6BD}
.pdf-status .r{margin-left:auto;color:#6B6A64}
.pdf-status .warn{color:#C8102E}
/* document body */
.pdf-h{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:14px;color:#23221E;margin:0 0 13px}
.pdf-p{font-size:10.5px;color:#3C3A34;line-height:1.9;margin:0}
.pdf-live{display:inline-block;padding:0 4px;border:1px solid #C8102E;background:#FBEEF0;color:#23221E}
/* dialog */
.pdf-behind{position:absolute;inset:0}
.pdf-behind::after{content:"";position:absolute;inset:0;background:rgba(35,34,30,0.28)}
.pdf-over-scrim{position:relative;width:800px;height:600px;display:flex;align-items:center;justify-content:center;overflow:hidden}
.pdf-over-scrim .pdf-dlg{position:relative;z-index:2}
.pdf-dlg{width:404px;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
.pdf-dlg-t{display:flex;align-items:center;height:26px;padding:0 10px;background:#FFFFFF;color:#23221E;border-bottom:2px solid #23221E;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:11.5px}
.pdf-dlg-b{padding:13px 15px;display:flex;flex-direction:column;gap:10px;font-size:10.5px;color:#3C3A34}
.pdf-dlg-a{display:flex;justify-content:flex-end;gap:6px;padding:0 15px 13px}
.pdf-kv{display:grid;grid-template-columns:82px 1fr;gap:4px 10px;align-items:baseline}
.pdf-kv .k{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.09em;text-transform:uppercase;color:#767368}
.pdf-in{border:1px solid #C7C6BD;background:#FFFFFF;padding:3px 7px;font-size:10.5px;color:#23221E;display:flex;align-items:center}
.pdf-in .cv{margin-left:auto;font-size:8px;color:#767368}
/* direction board */
.board{width:800px;background:#F3F3F0;padding:11px;display:flex;flex-direction:column;gap:11px}
.board h3{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px;color:#23221E;margin:0}
.board .why{font-size:10px;color:#3C3A34;margin:0}
.board .cost{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;color:#767368;margin:0}
.board .card{background:#FFFFFF;border:1px solid #C7C6BD;padding:9px 10px;display:flex;flex-direction:column;gap:6px}
.board .strip{border:1px solid #D7D6CE}
</style>
</helmet>
<div class="pdf"><div class="w-title"><img class="mmd-logo" src="mmd-logo-red.svg" alt="MMD" style="height:13px"><span class="app">MMD PDF</span><span class="name">BON-2026-0847 delivery note.pdf</span><span class="spring"></span><span class="ctl"><span>&minus;</span><span>&#9723;</span><span>&times;</span></span></div><div class="pdf-tools" style="height:40px"><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M2 6h6l2 2h8v9H2z"/></svg><span class="lb">Open</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3"/><path d="M6 17v-5h8v5"/></svg><span class="lb">Save</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 8V3h8v5"/><path d="M3 8h14v6h-3v4H6v-4H3z"/></svg><span class="lb">Print</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5V3h4"/><path d="M17 5V3h-4"/><path d="M3 15v2h4"/><path d="M17 15v2h-4"/><path d="M2 10h16"/></svg><span class="lb">Scan</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 6V3h3"/><path d="M17 6V3h-3"/><path d="M3 14v3h3"/><path d="M17 14v3h-3"/><path d="M6 9h8"/><path d="M6 12h5"/></svg><span class="lb">OCR</span></div><span class="pdf-rule"></span><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg><span class="lb">Select</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg><span class="lb">Text</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg><span class="lb">Mark</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg><span class="lb">Draw</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg><span class="lb">Shape</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg><span class="lb">Sign</span></div><span class="pdf-rule"></span><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 3h8v14H4z"/><path d="M12 6h4v11H8"/></svg><span class="lb">Pages</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 13l4 4"/><path d="M4 9a5 5 0 1010 0 5 5 0 10-10 0"/></svg><span class="lb">Search</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M7 6L3 10l4 4"/><path d="M3 10h9a4 4 0 010 8"/></svg><span class="lb">Undo</span></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>&minus;</i><i>+</i></span></div><div class="pdf-over"><svg width="14" height="14" viewBox="0 0 14 14" fill="#23221E"><circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/><circle cx="11" cy="7" r="1.2"/></svg></div></div><div class="pdf-work"><div class="pdf-strip"><svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="#23221E" stroke-width="1.3"><path d="M4 2l4 4-4 4"/></svg><span class="pn">1</span></div><div class="pdf-canvas"><div class="pdf-page"><h1 class="pdf-h">MMD STEEL GROUP &mdash; DELIVERY NOTE</h1><p class="pdf-p">Supplier: Fabrimetal SARL<br>Reference: BON-2026-0847<br>Quantity delivered: 12 tonnes<br>Vehicle: DK-4471-AB &middot; Driver: M. Sow<br><br>Received by: ______________________<br><br>Dakar plant &middot; corridor freight &middot; weighbridge ticket WB-4471</p></div></div></div><div class="pdf-status"><span class="">Ready</span><span class="r">1 OF 1 &middot; FIT WIDTH &middot; 100%</span></div></div></x-dc>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env python3
"""MMD PDF — design pages for the desktop editor at 800x600.
Stylesheet: the red-plate MMD shell design system, lifted verbatim from its
own source (portal-redesign session, canvas/build.mjs). Not retyped.
The screen budget at 800x600 drives every decision here:
600 - 28 title bar - 20 status bar = 552px for toolbar + document.
800 - page rail = document width.
A PDF editor carries about thirty tools. Thirty labelled tools need ~1400px.
Three directions resolve that differently; A/B/C are real alternatives, not
cosmetic variants of one answer.
"""
import json, os
OUT = os.path.dirname(os.path.abspath(__file__))
RP = open(os.path.join(OUT, "redplate.css")).read()
W, H = 800, 600
EXT = """
/* ---- MMD PDF: the parts the shell system has no component for yet ------
Named for adoption into the design system, not invented per screen.
Every colour below is a red-plate value; nothing new is introduced. */
.pdf{width:800px;height:600px;display:flex;flex-direction:column;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26);overflow:hidden}
/* tool strip */
.pdf-tools{display:flex;align-items:stretch;background:#F1F1EC;border-bottom:1px solid #D7D6CE}
.pdf-t{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:44px;color:#23221E;flex:none}
.pdf-t .lb{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.01em;text-transform:uppercase;line-height:1;white-space:nowrap}
.pdf-t.on{background:#C8102E;color:#FFFFFF}
.pdf-t.dim{color:#767368}
.pdf-rule{width:1px;background:#D7D6CE;margin:6px 3px;flex:none}
.pdf-end{margin-left:auto;display:flex;align-items:center;gap:5px;padding:0 6px;flex:none}
.pdf-zoom{display:flex;align-items:center;border:1px solid #C7C6BD;background:#FFFFFF}
.pdf-zoom b{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;font-weight:500;color:#23221E;padding:0 7px;line-height:19px}
.pdf-zoom i{width:19px;line-height:19px;text-align:center;font-style:normal;font-size:11px;color:#23221E;border-left:1px solid #C7C6BD}
.pdf-over{width:26px;display:flex;align-items:center;justify-content:center;color:#23221E;border-left:1px solid #D7D6CE;flex:none}
/* icon-only strip (direction B) */
.pdf-tools--icons .pdf-t{width:28px}
.pdf-tools--icons .pdf-t .lb{display:none}
/* tab strip (direction C) */
.pdf-tabs{display:flex;align-items:stretch;background:#E4E3DC;border-bottom:1px solid #D7D6CE;padding:0 4px}
.pdf-tab{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:0 11px;line-height:21px}
.pdf-tab.on{background:#FFFFFF;color:#23221E;font-weight:600;border-top:2px solid #C8102E;line-height:19px}
/* context bar — only present while a tool is armed */
.pdf-ctx{display:flex;align-items:center;gap:9px;height:28px;padding:0 9px;background:#FAFAF8;border-bottom:1px solid #E6E5DF;font-size:10px;color:#3C3A34}
.pdf-ctx .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.pdf-sw{display:flex;gap:3px}
.pdf-sw i{width:12px;height:12px;display:block;border:1px solid #C7C6BD}
.pdf-sw i.on{outline:1px solid #23221E;outline-offset:1px}
.pdf-pick{border:1px solid #C7C6BD;background:#FFFFFF;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;padding:1px 6px}
/* work area */
.pdf-work{flex:1;display:flex;min-height:0}
.pdf-strip{width:26px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:7px}
.pdf-strip .pn{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;line-height:1.25;text-align:center}
.pdf-rail{width:88px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;overflow:hidden}
.pdf-rail-h{display:flex;gap:8px;padding:6px 7px 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.pdf-rail-h .on{color:#23221E;font-weight:700}
.pdf-pg{margin:0 7px 5px;background:#FFFFFF;border:1px solid #C7C6BD}
.pdf-pg.here{box-shadow:inset 3px 0 0 #C8102E}
.pdf-pg .mini{height:64px;padding:5px 6px;overflow:hidden}
.pdf-pg .mini i{display:block;background:#C7C6BD;height:2px;margin-bottom:3px}
.pdf-pg .mini i.t{background:#23221E;height:3px;width:74%;margin-bottom:5px}
.pdf-pg .mini i.s{width:52%}
.pdf-pg .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;text-align:center;color:#3C3A34;line-height:16px;border-top:1px solid #E6E5DF}
.pdf-canvas{flex:1;background:#F3F3F0;display:flex;justify-content:center;overflow:hidden;padding:9px 0 0}
.pdf-page{width:100%;max-width:560px;background:#FFFFFF;border:1px solid #C7C6BD;padding:28px 34px}
/* status */
.pdf-status{display:flex;align-items:center;gap:12px;height:23px;padding:0 8px;background:#F1F1EC;border-top:1px solid #D7D6CE;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;flex:none}
.pdf-status .sep{width:1px;height:11px;background:#C7C6BD}
.pdf-status .r{margin-left:auto;color:#6B6A64}
.pdf-status .warn{color:#C8102E}
/* document body */
.pdf-h{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:14px;color:#23221E;margin:0 0 13px}
.pdf-p{font-size:10.5px;color:#3C3A34;line-height:1.9;margin:0}
.pdf-live{display:inline-block;padding:0 4px;border:1px solid #C8102E;background:#FBEEF0;color:#23221E}
/* dialog */
.pdf-behind{position:absolute;inset:0}
.pdf-behind::after{content:"";position:absolute;inset:0;background:rgba(35,34,30,0.28)}
.pdf-over-scrim{position:relative;width:800px;height:600px;display:flex;align-items:center;justify-content:center;overflow:hidden}
.pdf-over-scrim .pdf-dlg{position:relative;z-index:2}
.pdf-dlg{width:404px;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
.pdf-dlg-t{display:flex;align-items:center;height:26px;padding:0 10px;background:#FFFFFF;color:#23221E;border-bottom:2px solid #23221E;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:11.5px}
.pdf-dlg-b{padding:13px 15px;display:flex;flex-direction:column;gap:10px;font-size:10.5px;color:#3C3A34}
.pdf-dlg-a{display:flex;justify-content:flex-end;gap:6px;padding:0 15px 13px}
.pdf-kv{display:grid;grid-template-columns:82px 1fr;gap:4px 10px;align-items:baseline}
.pdf-kv .k{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.09em;text-transform:uppercase;color:#767368}
.pdf-in{border:1px solid #C7C6BD;background:#FFFFFF;padding:3px 7px;font-size:10.5px;color:#23221E;display:flex;align-items:center}
.pdf-in .cv{margin-left:auto;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;color:#767368}
/* direction board */
.board{width:800px;background:#F3F3F0;padding:11px;display:flex;flex-direction:column;gap:11px}
.board h3{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px;color:#23221E;margin:0}
.board .why{font-size:10px;color:#3C3A34;margin:0}
.board .cost{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.06em;text-transform:uppercase;color:#767368;margin:0}
.board .card{background:#FFFFFF;border:1px solid #C7C6BD;padding:9px 10px;display:flex;flex-direction:column;gap:6px}
.board .strip{border:1px solid #D7D6CE}
"""
# ── icons: one 20px grid, stroke only ────────────────────────────────────────
P = {
"Open": ['M2 6h6l2 2h8v9H2z'],
"Save": ['M3 3h11l3 3v11H3z', 'M6 3v5h7V3', 'M6 17v-5h8v5'],
"Print": ['M6 8V3h8v5', 'M3 8h14v6h-3v4H6v-4H3z'],
"Scan": ['M3 5V3h4', 'M17 5V3h-4', 'M3 15v2h4', 'M17 15v2h-4', 'M2 10h16'],
"OCR": ['M3 6V3h3', 'M17 6V3h-3', 'M3 14v3h3', 'M17 14v3h-3', 'M6 9h8', 'M6 12h5'],
"Select": ['M5 3l11 8-5 1 3 5-2 1-3-5-4 3z'],
"Text": ['M4 5h12', 'M10 5v11'],
"Mark": ['M4 15l3-9h2l3 9', 'M6 12h5', 'M14 15h3'],
"Draw": ['M3 17l3-1 9-9-2-2-9 9z'],
"Shape": ['M3 8h8v8H3z', 'M11 4h6v6h-6z'],
"Sign": ['M3 14c3-6 5 3 7-2s4 2 7-3', 'M3 17h14'],
"Stamp": ['M6 16h8', 'M7 13h6l-1-5a2 2 0 10-4 0z'],
"Image": ['M3 4h14v12H3z', 'M3 13l4-4 3 3 3-3 4 4'],
"Crop": ['M6 2v12h12', 'M2 6h12v12'],
"Pages": ['M4 3h8v14H4z', 'M12 6h4v11H8'],
"Search": ['M13 13l4 4', 'M4 9a5 5 0 1010 0 5 5 0 10-10 0'],
"Undo": ['M7 6L3 10l4 4', 'M3 10h9a4 4 0 010 8'],
"Redo": ['M13 6l4 4-4 4', 'M17 10H8a4 4 0 000 8'],
"Merge": ['M3 5h6v4H3z', 'M11 11h6v4h-6z', 'M6 9v3h5'],
"Flatten":['M6 3h8v7H6z', 'M3 13h14', 'M3 16h14'],
}
def ico(n, on=False, dim=False):
col = "#FFFFFF" if on else ("#767368" if dim else "#23221E")
d = "".join(f'<path d="{p}"/>' for p in P.get(n, ['M4 4h12v12H4z']))
return (f'<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="{col}" '
f'stroke-width="1.35" stroke-linecap="square">{d}</svg>')
def T(n, on=False, dim=False):
c = "pdf-t on" if on else ("pdf-t dim" if dim else "pdf-t")
return f'<div class="{c}">{ico(n, on, dim)}<span class="lb">{n}</span></div>'
ZOOM = ('<span class="pdf-zoom"><b>FIT WIDTH</b><i>&minus;</i><i>+</i></span>')
OVER = ('<div class="pdf-over"><svg width="14" height="14" viewBox="0 0 14 14" fill="#23221E">'
'<circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/><circle cx="11" cy="7" r="1.2"/></svg></div>')
def title(name="BON-2026-0847 delivery note.pdf", tag=""):
t = f'<span class="new">{tag}</span>' if tag else ''
return (f'<div class="w-title"><img class="mmd-logo" src="mmd-logo-red.svg" alt="MMD" '
f'style="height:13px"><span class="app">MMD PDF</span>'
f'<span class="name">{name}</span>{t}<span class="spring"></span>'
f'<span class="ctl"><span>&minus;</span><span>&#9723;</span><span>&times;</span></span></div>')
def strip_collapsed(n="1"):
return (f'<div class="pdf-strip"><svg width="11" height="11" viewBox="0 0 12 12" fill="none" '
f'stroke="#23221E" stroke-width="1.3"><path d="M4 2l4 4-4 4"/></svg>'
f'<span class="pn">{n}</span></div>')
def rail_open(pages=("1", "2", "3")):
pg = "".join(
f'<div class="pdf-pg{" here" if i == 0 else ""}"><div class="mini">'
f'<i class="t"></i><i></i><i></i><i class="s"></i><i></i></div>'
f'<div class="n">{p}</div></div>' for i, p in enumerate(pages))
return (f'<div class="pdf-rail"><div class="pdf-rail-h"><span class="on">Pages</span>'
f'<span>Marks</span></div>{pg}</div>')
DOC = ('<h1 class="pdf-h">MMD STEEL GROUP &mdash; DELIVERY NOTE</h1>'
'<p class="pdf-p">Supplier: Fabrimetal SARL<br>Reference: BON-2026-0847<br>'
'Quantity delivered: 12 tonnes<br>Vehicle: DK-4471-AB &middot; Driver: M. Sow<br><br>'
'Received by: ______________________<br><br>'
'Dakar plant &middot; corridor freight &middot; weighbridge ticket WB-4471</p>')
DOC_EDIT = ('<h1 class="pdf-h">MMD STEEL GROUP &mdash; DELIVERY NOTE</h1>'
'<p class="pdf-p">Supplier: Fabrimetal SARL<br>Reference: BON-2026-0847<br>'
'<span class="pdf-live">Quantity delivered: 19 tonnes</span><br>'
'Vehicle: DK-4471-AB &middot; Driver: M. Sow<br><br>'
'Received by: ______________________<br><br>'
'Dakar plant &middot; corridor freight &middot; weighbridge ticket WB-4471</p>')
def status(left, right, warn=False):
cls = "warn" if warn else ""
return (f'<div class="pdf-status"><span class="{cls}">{left}</span>'
f'<span class="r">{right}</span></div>')
HEAD = """<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
<style>
%s
%s
</style>
</helmet>
""" % (RP, EXT)
TAIL = "</x-dc>\n</body>\n</html>\n"
def w(name, inner):
open(os.path.join(OUT, name), "w", encoding="utf-8").write(HEAD + inner + TAIL)
# ── Direction A: one labelled strip, fourteen tools, rest behind overflow ────
A_TOOLS = ["Open", "Save", "Print", "Scan", "OCR"]
A_EDIT = ["Select", "Text", "Mark", "Draw", "Shape", "Sign"]
A_DOC = ["Pages", "Search", "Undo"]
def strip_A(active="Select", h=40):
return (f'<div class="pdf-tools" style="height:{h}px">'
+ "".join(T(t) for t in A_TOOLS) + '<span class="pdf-rule"></span>'
+ "".join(T(t, t == active) for t in A_EDIT) + '<span class="pdf-rule"></span>'
+ "".join(T(t) for t in A_DOC)
+ f'<div class="pdf-end">{ZOOM}</div>{OVER}</div>')
w("Main.dc.html",
'<div class="pdf">' + title() + strip_A("Select") +
'<div class="pdf-work">' + strip_collapsed() +
'<div class="pdf-canvas"><div class="pdf-page">' + DOC + '</div></div></div>' +
status("Ready", "1 OF 1 &middot; FIT WIDTH &middot; 100%") + '</div>')
# ── Editing text, context bar present ───────────────────────────────────────
ctx = ('<div class="pdf-ctx"><span class="lbl">Colour</span>'
'<span class="pdf-sw"><i class="on" style="background:#23221E"></i>'
'<i style="background:#C8102E"></i><i style="background:#3C3A34"></i>'
'<i style="background:#767368"></i><i style="background:#FFFFFF"></i></span>'
'<span class="lbl">Size</span><span class="pdf-pick">11 pt</span>'
'<span class="pdf-pick">B</span><span class="pdf-pick">I</span>'
'<span class="spring"></span>'
'<span class="lbl">Enter keeps it &middot; Esc throws it away</span></div>')
w("EditText.dc.html",
'<div class="pdf">' + title(tag="Changed") + strip_A("Text") + ctx +
'<div class="pdf-work">' + strip_collapsed() +
'<div class="pdf-canvas"><div class="pdf-page">' + DOC_EDIT + '</div></div></div>' +
status("The old quantity is removed from the file when you save",
"1 OF 1 &middot; FIT WIDTH &middot; 100%", warn=True) + '</div>')
# ── Rail open, three pages ──────────────────────────────────────────────────
w("PageRail.dc.html",
'<div class="pdf">' + title("Weighbridge tickets week 34.pdf") + strip_A("Select") +
'<div class="pdf-work">' + rail_open() +
'<div class="pdf-canvas"><div class="pdf-page">' + DOC + '</div></div></div>' +
status("Ready", "1 OF 3 &middot; FIT WIDTH &middot; 100%") + '</div>')
# ── Nothing open ────────────────────────────────────────────────────────────
rows = "".join(
f'<tr><td class="ref"{" style=box-shadow:inset~3px~0~0~#C8102E".replace(chr(126),chr(32)) if i == 0 else ""}>{r}</td>'
f'<td>{d}</td><td class="num">{n}</td><td><span class="w-state">{s}</span></td></tr>'
for i, (r, d, n, s) in enumerate([
("BON-2026-0847", "Delivery note &mdash; Fabrimetal SARL", "1", "Changed"),
("INV-4471", "Supplier invoice &mdash; Fabrimetal SARL", "3", "Signed"),
("WB-4471", "Weighbridge ticket &mdash; Dakar plant", "1", "Scanned"),
("INSP-0912", "Mill inspection &mdash; Kigali", "8", "Read"),
("PO-AO-2231", "Refractory bricks &mdash; Vesuvius Angola", "2", "Read"),
("BON-2026-0839", "Delivery note &mdash; Continental Sahel", "1", "Signed"),
("INV-4402", "Freight invoice &mdash; corridor Dakar&ndash;Bamako", "2", "Read"),
("WB-4468", "Weighbridge ticket &mdash; Dakar plant", "1", "Scanned"),
("PO-SN-24099", "Diesel gensets &mdash; Groupe S&eacute;n&eacute;gal &Eacute;nergie", "4", "Read"),
("BON-2026-0831", "Delivery note &mdash; SMS Concast", "1", "Read"),
("INSP-0908", "Mill inspection &mdash; Dakar", "6", "Read"),
("WB-4455", "Weighbridge ticket &mdash; Dakar plant", "1", "Scanned"),
("INV-4377", "Supplier invoice &mdash; RHI Magnesita", "3", "Signed"),
("PO-RW-24107", "Rolling mill rollers &mdash; Danieli Service", "2", "Read"),
("BON-2026-0824", "Delivery note &mdash; Nexans Angola", "1", "Read"),
]))
w("Nothing.dc.html",
'<div class="pdf">' + title("No document open") +
f'<div class="pdf-tools" style="height:40px">'
+ "".join(T(t) for t in A_TOOLS) + '<span class="pdf-rule"></span>'
+ "".join(T(t, dim=True) for t in A_EDIT) + '<span class="pdf-rule"></span>'
+ "".join(T(t, dim=True) for t in A_DOC)
+ f'<div class="pdf-end">{ZOOM}</div>{OVER}</div>'
+ '<div class="pdf-work"><div class="pdf-canvas" style="padding:0"><div style="width:100%">'
'<div class="w-toolbar"><h2 class="w-h1">Recent</h2>'
'<span class="w-count">on this machine</span>'
'<span class="w-filters"><span class="w-chip">Open a file&hellip;</span>'
'<span class="w-cta">Scan a document</span></span></div>'
'<table class="w-tbl"><thead><tr><th>Reference</th><th>Document</th>'
'<th class="num">Pages</th><th>State</th></tr></thead><tbody>' + rows +
'</tbody></table></div></div></div>' +
status("Pick a document, or scan one", "NOTHING OPEN") + '</div>')
# ── Saving over the original ────────────────────────────────────────────────
def behind(doc=DOC, active="Select", st=("Ready", "1 OF 1 &middot; FIT WIDTH &middot; 100%")):
return ('<div class="pdf-behind"><div class="pdf" style="box-shadow:none;border:0">'
+ title() + strip_A(active)
+ '<div class="pdf-work">' + strip_collapsed()
+ '<div class="pdf-canvas"><div class="pdf-page">' + doc + '</div></div></div>'
+ status(*st) + '</div></div>')
w("Save.dc.html",
'<div class="pdf-over-scrim">' + behind(DOC_EDIT, "Text",
("The old quantity is removed from the file when you save",
"1 OF 1 &middot; FIT WIDTH &middot; 100%")) +
'<div class="pdf-dlg">'
'<div class="pdf-dlg-t">Save</div><div class="pdf-dlg-b">'
'<div>Write the change into <b>BON-2026-0847 delivery note.pdf</b>?</div>'
'<div class="pdf-kv">'
'<span class="k">Was</span><span>Quantity delivered: 12 tonnes</span>'
'<span class="k">Becomes</span><span>Quantity delivered: 19 tonnes</span>'
'<span class="k">The old text</span><span>is taken out of the file, not covered over</span>'
'</div></div><div class="pdf-dlg-a">'
'<span class="w-chip">Cancel</span><span class="w-chip">Keep both</span>'
'<span class="w-cta">Replace</span></div></div></div>')
# ── Scan ────────────────────────────────────────────────────────────────────
def field(k, v, note=""):
n = f'<span class="cv">{note}</span>' if note else ''
return f'<span class="k">{k}</span><span class="pdf-in">{v}{n}</span>'
w("Scan.dc.html",
'<div class="pdf-over-scrim">' + behind() + '<div class="pdf-dlg">'
'<div class="pdf-dlg-t">Scan</div><div class="pdf-dlg-b">'
'<div>Pages arrive in the document you have open.</div>'
'<div class="pdf-kv">'
+ field("Copier", "Ricoh IM C3000 &mdash; Dakar plant", "ready")
+ field("Feed", "Both sides")
+ field("Colour", "Greyscale")
+ field("Detail", "300 dpi", "print")
+ '</div>'
'<div><span class="w-state">Text search on</span> &nbsp;French and English</div>'
'</div><div class="pdf-dlg-a">'
'<span class="w-chip">Cancel</span><span class="w-cta">Scan</span></div></div></div>')
# ── The three directions ────────────────────────────────────────────────────
def strip_B(active="Select"):
tools = A_TOOLS + A_EDIT + ["Stamp", "Image", "Crop", "Merge", "Flatten"] + A_DOC + ["Redo"]
return ('<div class="pdf-tools pdf-tools--icons" style="height:30px">'
+ "".join(ico_t(t, t == active) for t in tools)
+ f'<div class="pdf-end">{ZOOM}</div></div>')
def ico_t(n, on=False):
c = "pdf-t on" if on else "pdf-t"
return f'<div class="{c}">{ico(n, on)}</div>'
def strip_C(tab="Edit"):
tabs = ("Document", "Edit", "Pages")
row = {"Document": A_TOOLS + ["Merge", "Flatten"],
"Edit": A_EDIT + ["Stamp", "Image", "Crop"],
"Pages": ["Pages", "Search", "Undo", "Redo"]}[tab]
return ('<div class="pdf-tabs">' + "".join(
f'<span class="pdf-tab{" on" if t == tab else ""}">{t}</span>' for t in tabs)
+ '</div><div class="pdf-tools" style="height:34px">'
+ "".join(T(t, t == "Select") for t in row)
+ f'<div class="pdf-end">{ZOOM}</div></div>')
board = f'''<div class="board">
<div class="card">
<h3>A &mdash; one row, named tools</h3>
<div class="strip">{strip_A("Select", 40)}</div>
<p class="why">Fourteen tools carry their name. Everything else &mdash; stamp, image, crop, merge, flatten, redo &mdash; lives behind the three dots on the right.</p>
<p class="cost">40px of chrome &middot; 512px left for the page &middot; 16 of 30 tools hidden</p>
</div>
<div class="card">
<h3>B &mdash; icons only, nothing hidden</h3>
<div class="strip">{strip_B("Select")}</div>
<p class="why">All thirty fit, name on hover. Fastest once someone knows the icons; a plant clerk opening it for the first time has to guess.</p>
<p class="cost">30px of chrome &middot; 522px left for the page &middot; nothing hidden</p>
</div>
<div class="card">
<h3>C &mdash; three tabs, named tools, nothing hidden</h3>
<div class="strip">{strip_C("Edit")}</div>
<p class="why">Document, Edit, Pages. Every tool keeps its name and every tool is reachable, at the price of one click to change tab and a second bar of chrome.</p>
<p class="cost">56px of chrome &middot; 496px left for the page &middot; nothing hidden, one click deeper</p>
</div>
</div>'''
w("Directions.dc.html", board)
# ── canvas ──────────────────────────────────────────────────────────────────
GX, GY = W + 90, H + 150
canvas = {
"artboards": [
{"file": "Directions.dc.html", "x": 0, "y": 0, "w": W, "h": 470,
"title": "Three ways to fit the toolbar — pick one"},
{"file": "Main.dc.html", "x": GX, "y": 0, "w": W, "h": H, "title": "A document open"},
{"file": "EditText.dc.html", "x": 2 * GX, "y": 0, "w": W, "h": H, "title": "Changing text on the page"},
{"file": "PageRail.dc.html", "x": 0, "y": GY, "w": W, "h": H, "title": "Page list open"},
{"file": "Nothing.dc.html", "x": GX, "y": GY, "w": W, "h": H, "title": "Nothing open yet"},
{"file": "Save.dc.html", "x": 2 * GX, "y": GY, "w": W, "h": H, "title": "Saving over the original"},
{"file": "Scan.dc.html", "x": GX, "y": 2 * GY, "w": W, "h": H, "title": "Scanning — not built yet"},
],
"annotations": [
{"id": "brief", "x": 0, "y": -150, "w": 660,
"text": "MMD PDF — the Windows PDF editor for staff.\nEvery frame is 800x600, the plant PC screen. Styling is the new MMD shell system, taken from its own source. Official MMD lockup only.\n600 minus 28 title bar minus 20 status bar leaves 552px for toolbar and page."},
{"id": "decision", "x": 0, "y": 490, "w": 620,
"text": "The one thing that needs your answer: a PDF editor has about thirty tools and thirty named tools need roughly 1400px. A, B and C give up different things. Mark the one you want and the rest follows from it."},
{"id": "scan-note", "x": GX, "y": 2 * GY - 90, "w": 420,
"text": "Scanning does not exist in the application yet. This is the design for it, not a picture of something that works."},
{"id": "edit-note", "x": 2 * GX, "y": -90, "w": 430,
"text": "This is the moment the old wording is destroyed. The status bar says so, and Enter/Esc are spelled out because both were guessed wrong during testing."},
],
"launch": {"view": "canvas"},
}
json.dump(canvas, open(os.path.join(OUT, "canvas.json"), "w"), indent=2)
print("written 7 artboards")
+93
View File
@@ -0,0 +1,93 @@
{
"artboards": [
{
"file": "Directions.dc.html",
"x": 0,
"y": 0,
"w": 800,
"h": 470,
"title": "Three ways to fit the toolbar \u2014 pick one"
},
{
"file": "Main.dc.html",
"x": 890,
"y": 0,
"w": 800,
"h": 600,
"title": "A document open"
},
{
"file": "EditText.dc.html",
"x": 1780,
"y": 0,
"w": 800,
"h": 600,
"title": "Changing text on the page"
},
{
"file": "PageRail.dc.html",
"x": 0,
"y": 750,
"w": 800,
"h": 600,
"title": "Page list open"
},
{
"file": "Nothing.dc.html",
"x": 890,
"y": 750,
"w": 800,
"h": 600,
"title": "Nothing open yet"
},
{
"file": "Save.dc.html",
"x": 1780,
"y": 750,
"w": 800,
"h": 600,
"title": "Saving over the original"
},
{
"file": "Scan.dc.html",
"x": 890,
"y": 1500,
"w": 800,
"h": 600,
"title": "Scanning \u2014 not built yet"
}
],
"annotations": [
{
"id": "brief",
"x": 0,
"y": -150,
"w": 660,
"text": "MMD PDF \u2014 the Windows PDF editor for staff.\nEvery frame is 800x600, the plant PC screen. Styling is the new MMD shell system, taken from its own source. Official MMD lockup only.\n600 minus 28 title bar minus 20 status bar leaves 552px for toolbar and page."
},
{
"id": "decision",
"x": 0,
"y": 490,
"w": 620,
"text": "The one thing that needs your answer: a PDF editor has about thirty tools and thirty named tools need roughly 1400px. A, B and C give up different things. Mark the one you want and the rest follows from it."
},
{
"id": "scan-note",
"x": 890,
"y": 1410,
"w": 420,
"text": "Scanning does not exist in the application yet. This is the design for it, not a picture of something that works."
},
{
"id": "edit-note",
"x": 1780,
"y": -90,
"w": 430,
"text": "This is the moment the old wording is destroyed. The status bar says so, and Enter/Esc are spelled out because both were guessed wrong during testing."
}
],
"launch": {
"view": "canvas"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

+103
View File
@@ -0,0 +1,103 @@
body{margin:0;background:#F3F3F0;font-family:"PP Neue Machina","Neue Machina",system-ui,-apple-system,"Segoe UI",sans-serif;color:#3C3A34;-webkit-font-smoothing:antialiased}
a{color:#C8102E} a:hover{color:#8E0B20}
*{box-sizing:border-box}
.root{padding:16px;background:#F3F3F0}
.shell{width:1180px;display:flex;flex-direction:column;font-size:11.5px;background:#FFFFFF;border:1px solid #C7C6BD}
.s-title{display:flex;align-items:center;gap:10px;padding:0 10px;height:30px;background:#C8102E;color:#fff;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px}
.s-title .mmd-logo{height:19px;width:auto;display:block;flex:none;fill:#fff}
.spring{flex:1}
.s-who{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.07em;text-transform:uppercase;color:rgba(255,255,255,0.85)}
.s-signout{font-size:10px;padding:1px 0;color:#fff;text-decoration:underline;text-underline-offset:2px}
.s-bar{display:flex;align-items:stretch;gap:2px;padding:0 5px;background:#F7F7F3;border-bottom:1px solid #D7D6CE}
.s-item{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;white-space:nowrap;border:1px solid transparent;color:#3C3A34}
.s-item .cv{font-size:8px;opacity:0.5}
.s-item .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;padding:0 4px;border:1px solid #23221E;color:#23221E;background:#FFFFFF;line-height:13px}
.s-item.on{background:#FFFFFF;border-color:#D7D6CE;border-top:2px solid #C8102E;border-bottom-color:#FFFFFF;margin-bottom:-1px;font-weight:700;color:#23221E}
.s-item.on .n{border-color:#23221E;color:#23221E}
.s-tools{margin-left:auto;display:flex;align-items:center;gap:6px;padding-right:4px}
.s-search{width:168px;padding:2px 7px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
.s-btn{padding:2px 9px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
.s-desk{position:relative;background:#D4D0C6;overflow:hidden}
.s-desk.tiled{display:grid;grid-template-columns:1.35fr 1fr;grid-template-rows:auto auto;gap:10px;padding:10px;align-items:start}
.s-desk.stacked{height:492px}
.s-desk.oneup{padding:10px}
.s-task{display:flex;align-items:center;gap:10px;padding:4px 8px;font-size:10.5px;background:#F1F1EC;border-top:1px solid #D7D6CE}
.s-task .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
.s-group{display:inline-flex;align-items:stretch;border:1px solid #D7D6CE;background:#FFFFFF}
.s-group .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:2px 7px;border-right:1px solid #D7D6CE;display:flex;align-items:center;background:#F7F7F3}
.s-group .win{padding:2px 8px;font-size:10px;color:#23221E;border-right:1px solid #D7D6CE;display:flex;align-items:center;gap:5px;background:#F1F1EC}
.s-group .win:last-child{border-right:0}
.s-group .win.on{box-shadow:inset 3px 0 0 #C8102E;font-weight:700;background:#FFFFFF}
.s-group .win.min{color:#3C3A34;background:#FFFFFF;outline:1px dashed #6B6A64;outline-offset:-3px}
.s-task .tile{font-size:10px;padding:1px 8px;border:1px solid #C7C6BD;background:#FFFFFF;color:#3C3A34}
/* windows — title bar made visible: darker plate, ink name, clear border; front = red top edge + shadow */
.win-frame{background:#FFFFFF;border:1px solid #B9B8AE;display:flex;flex-direction:column;min-width:0;box-shadow:0 1px 2px rgba(35,34,30,0.12)}
.win-frame.front{border-color:#767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
.stacked .win-frame{position:absolute}
.w-title{display:flex;align-items:center;gap:8px;height:28px;padding:0 6px 0 8px;background:#E4E3DC;border-bottom:1px solid #C9C6BC;font-size:11px;color:#3C3A34}
.front .w-title{background:#FFFFFF;color:#23221E;border-top:3px solid #C8102E;height:31px}
.w-title .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#23221E;font-weight:700}
.w-title .app::after{content:"/";margin-left:8px;color:#6B6A64}
.w-title .name{font-family:Fraunces,Georgia,serif;font-weight:600;font-size:12px;color:#3C3A34}
.front .w-title .name{color:#23221E;font-weight:700}
.w-title .new{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#23221E;background:#E4E3DC;padding:0 6px;line-height:15px}
.w-title .ctl{display:flex;gap:8px}
.w-title .ctl span{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#23221E;background:transparent;border:0}
.w-body{padding:0;overflow:hidden}
.w-toolbar{display:flex;align-items:center;gap:8px;padding:5px 8px;background:#FAFAF8;border-bottom:1px solid #E6E5DF}
.w-h1{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:13px;letter-spacing:-0.01em;margin:0;color:#23221E}
.w-count{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#6B6A64}
.w-count b{color:#C8102E;font-weight:600}
.w-filters{margin-left:auto;display:flex;gap:4px}
.w-chip{padding:1px 7px;font-size:9.5px;font-family:"JetBrains Mono",ui-monospace,monospace;border:1px solid #D7D6CE;background:#FFFFFF;color:#3C3A34}
.w-chip.on{border-color:#767368;color:#23221E;font-weight:600}
.w-cta{padding:2px 9px;font-size:10px;background:#C8102E;color:#FFFFFF;border:1px solid #C8102E;font-weight:600}
table.w-tbl{width:100%;border-collapse:collapse;font-size:10.5px;background:#FFFFFF}
.w-tbl th{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;text-align:left;padding:4px 8px;white-space:nowrap;background:#F1F1EC;color:#23221E;border-bottom:1px solid #D7D6CE;border-right:1px solid #E6E5DF}
.w-tbl td{padding:4px 8px;border-bottom:1px solid #E6E5DF;border-right:1px solid #E6E5DF;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
.w-tbl td:last-child,.w-tbl th:last-child{border-right:0}
.w-tbl tr:nth-child(even) td{background:#FAFAF8}
.w-tbl td.num{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;text-align:right}
.w-tbl th.num{text-align:right}
.w-tbl td.ref{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.03em}
.w-tbl tr.sel td{background:#F7F7F3;box-shadow:inset 3px 0 0 #C8102E}
.w-state{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.07em;text-transform:uppercase;padding:1px 5px;background:#F1F1EC;color:#23221E}
.rec{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:0}
.rec .col{padding:6px 10px 8px;border-right:1px solid #E6E5DF}
.rec .col:last-child{border-right:0}
.rec h4{margin:4px 0 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;font-weight:600;border-bottom:1px solid #E6E5DF;padding-bottom:3px}
.kv{display:grid;grid-template-columns:96px 1fr;gap:2px 8px;font-size:10.5px;margin:0}
.kv dt{color:#767368}
.kv dd{margin:0;color:#23221E}
.kv dd.mono{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;font-variant-numeric:tabular-nums}
.rec-actions{display:flex;gap:6px;padding:6px 10px;border-top:1px solid #E6E5DF;background:#FAFAF8}
.rec-actions span{padding:2px 9px;font-size:10px;border:1px solid #C7C6BD;background:#FFFFFF;color:#23221E}
.rec-actions span.primary{color:#FFFFFF;background:#C8102E;border:1px solid #C8102E;font-weight:600}
.steps{display:flex;gap:0;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;border-bottom:1px solid #E6E5DF}
.steps span{padding:3px 8px;border-right:1px solid #E6E5DF;color:#6B6A64}
.steps span.done{color:#767368}
.steps span.now{color:#C8102E;font-weight:700;box-shadow:inset 0 -2px 0 #C8102E}
.rep{padding:6px 10px}
.rep .row{display:grid;grid-template-columns:150px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid #E6E5DF;font-size:10.5px}
.rep .row:last-child{border-bottom:0}
.rep .name{color:#23221E;font-weight:600}
.rep .name small{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.08em;text-transform:uppercase;color:#6B6A64;font-weight:400}
.rep .hl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;font-variant-numeric:tabular-nums}
/* title-bar option closeups */
.opt{width:600px;padding:16px;background:#D4D0C6}
.opt .cap{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;margin:0 0 8px}
.opt .win-frame{margin-bottom:14px}
.opt .win-frame:last-child{margin-bottom:0}
/* Option B: ink strip */
.optB .front .w-title{background:#3C3A34;color:#fff;border-top:0;height:28px;border-bottom:1px solid #23221E}
.optB .front .w-title .name{color:#fff}
.optB .front .w-title .app{color:#fff}
.optB .front .w-title .app::after{color:rgba(255,255,255,0.35)}
.optB .front .w-title .ctl span{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.6)}
.optB .front .w-title .new{color:#fff;border-color:rgba(255,255,255,0.7)}
/* Option C: ruled */
.optC .w-title{background:#FFFFFF;border-bottom:2px solid #23221E;height:28px}
.optC .front .w-title{background:#FFFFFF;border-top:3px solid #C8102E;height:31px;border-bottom:2px solid #23221E}
.optC .win-frame{border-color:#23221E}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB