diff --git a/Controls/SegmentMeter.cs b/Controls/SegmentMeter.cs
new file mode 100644
index 0000000..46e56bd
--- /dev/null
+++ b/Controls/SegmentMeter.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using ytLive.Helpers;
+
+namespace ytLive.Controls;
+
+/// A segmented level ladder for the audio dock.
+///
+/// Drawn in ONE pass. Twenty-four Rectangles per meter would
+/// mean hundreds of elements updating on the UI thread; a single drawing call does not. The
+/// consumer updates at ~20 Hz — music-level smoothness is not needed and the
+/// paint cost should never compete with the compositor.
+///
+/// Level is a 0…1 display fraction (see ), never a gain setting: the
+/// ladder shows what was measured.
+public sealed class SegmentMeter : FrameworkElement
+{
+ public static readonly DependencyProperty LevelProperty = DependencyProperty.Register(
+ nameof(Level), typeof(double), typeof(SegmentMeter),
+ new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ /// Peak-hold mark, 0…1. Falls back slowly so a clip stays visible long enough to see.
+ public static readonly DependencyProperty PeakProperty = DependencyProperty.Register(
+ nameof(Peak), typeof(double), typeof(SegmentMeter),
+ new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty SegmentsProperty = DependencyProperty.Register(
+ nameof(Segments), typeof(int), typeof(SegmentMeter),
+ new FrameworkPropertyMetadata(24, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
+ nameof(Orientation), typeof(Orientation), typeof(SegmentMeter),
+ new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public double Level { get => (double)GetValue(LevelProperty); set => SetValue(LevelProperty, value); }
+ public double Peak { get => (double)GetValue(PeakProperty); set => SetValue(PeakProperty, value); }
+ public int Segments { get => (int)GetValue(SegmentsProperty); set => SetValue(SegmentsProperty, value); }
+ public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); }
+
+ public SegmentMeter()
+ {
+ // Token-driven, not literal: the ladder cannot drift from DESIGN.md.
+ SetResourceReference(NominalBrushProperty, "Brush.Meter.Nominal");
+ SetResourceReference(CautionBrushProperty, "Brush.Meter.Caution");
+ SetResourceReference(PeakBrushProperty, "Brush.Meter.Peak");
+ SetResourceReference(DimBrushProperty, "Brush.SurfaceContainerHighest");
+ }
+
+ private static readonly DependencyProperty NominalBrushProperty = DependencyProperty.Register(
+ "NominalBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Cyan, FrameworkPropertyMetadataOptions.AffectsRender));
+ private static readonly DependencyProperty CautionBrushProperty = DependencyProperty.Register(
+ "CautionBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Orange, FrameworkPropertyMetadataOptions.AffectsRender));
+ private static readonly DependencyProperty PeakBrushProperty = DependencyProperty.Register(
+ "PeakBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Red, FrameworkPropertyMetadataOptions.AffectsRender));
+ private static readonly DependencyProperty DimBrushProperty = DependencyProperty.Register(
+ "DimBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.DimGray, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ private Brush NominalBrush => (Brush)GetValue(NominalBrushProperty);
+ private Brush CautionBrush => (Brush)GetValue(CautionBrushProperty);
+ private Brush PeakBrush => (Brush)GetValue(PeakBrushProperty);
+ private Brush DimBrush => (Brush)GetValue(DimBrushProperty);
+
+ private const double Gap = 2;
+
+ protected override void OnRender(DrawingContext dc)
+ {
+ var count = Math.Max(1, Segments);
+ var vertical = Orientation == Orientation.Vertical;
+ var extent = vertical ? ActualHeight : ActualWidth;
+ var thickness = vertical ? ActualWidth : ActualHeight;
+ if (extent <= 0 || thickness <= 0) return;
+
+ var segmentSize = (extent - Gap * (count - 1)) / count;
+ if (segmentSize <= 0) return;
+
+ var lit = Math.Clamp(Level, 0, 1) * count;
+
+ for (var i = 0; i < count; i++)
+ {
+ // Segment 0 sits at the bottom (vertical) or the left (horizontal).
+ var fraction = (i + 1) / (double)count;
+ var brush = fraction >= MeterScale.ToFraction(MeterScale.PeakDb) ? PeakBrush
+ : fraction >= MeterScale.ToFraction(MeterScale.CautionDb) ? CautionBrush
+ : NominalBrush;
+
+ if (i >= lit) brush = DimBrush;
+
+ var offset = i * (segmentSize + Gap);
+ var rect = vertical
+ ? new Rect(0, extent - offset - segmentSize, thickness, segmentSize)
+ : new Rect(offset, 0, segmentSize, thickness);
+ dc.DrawRectangle(brush, null, rect);
+ }
+
+ // Peak-hold: a 2px marker at the held level, drawn above the ladder.
+ var peak = Math.Clamp(Peak, 0, 1);
+ if (peak <= 0) return;
+ var line = vertical
+ ? new Rect(0, extent - peak * extent - 1, thickness, 2)
+ : new Rect(peak * extent - 1, 0, 2, thickness);
+ dc.DrawRectangle(PeakBrush, null, line);
+ }
+}
diff --git a/Helpers/MeterScale.cs b/Helpers/MeterScale.cs
new file mode 100644
index 0000000..7a2991f
--- /dev/null
+++ b/Helpers/MeterScale.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace ytLive.Helpers;
+
+/// Which colour band a level falls in (DESIGN.md meter thresholds).
+public enum MeterBand
+{
+ Nominal,
+ Caution,
+ Peak,
+}
+
+/// The mathematics of the dock's segment meter, kept out of the control so it can be
+/// tested: a segment ladder is only honest if the band it paints is the level it measured.
+///
+/// Bands follow DESIGN.md: nominal to −12 dB, caution to −3 dB, peak above. The display
+/// fraction spreads −60…0 dBFS across 0…1 — the same scale AudioLevelMeter.ToDisplay uses,
+/// so the ladder and the old bars cannot disagree.
+public static class MeterScale
+{
+ public const float FloorDb = -60f;
+ public const float CautionDb = -12f;
+ public const float PeakDb = -3f;
+
+ /// dBFS → 0…1 across the whole ladder.
+ public static float ToFraction(float dbfs)
+ => Math.Clamp(1f + Math.Clamp(dbfs, FloorDb, 0f) / 60f, 0f, 1f);
+
+ /// The band boundaries are INCLUSIVE at the bottom edge (a meter reading exactly
+ /// −12.0 dB is cautious, exactly −3.0 is peaking), matching the ranges as DESIGN.md states
+ /// them: "−12 … −3" and "−3 … 0+".
+ public static MeterBand BandFor(float dbfs)
+ => dbfs >= PeakDb ? MeterBand.Peak
+ : dbfs >= CautionDb ? MeterBand.Caution
+ : MeterBand.Nominal;
+
+ /// Peak hold: jumps up instantly to a new maximum and falls back at
+ /// — a moment-to-moment bar hides clipping, which is the one
+ /// audio fault that cannot be fixed afterwards.
+ public static float NextPeak(float held, float incoming, float seconds, float decayPerSecond = 0.35f)
+ {
+ if (incoming >= held) return incoming;
+ return Math.Max(0f, held - decayPerSecond * Math.Max(0f, seconds));
+ }
+}
diff --git a/ytLive.Tests/MeterScaleTests.cs b/ytLive.Tests/MeterScaleTests.cs
new file mode 100644
index 0000000..e852d25
--- /dev/null
+++ b/ytLive.Tests/MeterScaleTests.cs
@@ -0,0 +1,35 @@
+using Xunit;
+using ytLive.Helpers;
+
+namespace ytLive.Tests;
+
+/// A segment ladder is only useful if the band it paints is the level it measured. The
+/// thresholds are DESIGN.md's (nominal / caution / peak), and peak-hold is what makes clipping
+/// visible — a bar that follows the moment hides the one audio fault that cannot be fixed later.
+///
+public class MeterScaleTests
+{
+ [Fact]
+ public void Bands_peak_hold_and_the_display_scale_follow_the_standard()
+ {
+ // Bands at the documented thresholds.
+ Assert.Equal(MeterBand.Nominal, MeterScale.BandFor(-30f));
+ Assert.Equal(MeterBand.Nominal, MeterScale.BandFor(-12.1f));
+ Assert.Equal(MeterBand.Caution, MeterScale.BandFor(-12f));
+ Assert.Equal(MeterBand.Caution, MeterScale.BandFor(-6f));
+ Assert.Equal(MeterBand.Peak, MeterScale.BandFor(-3f));
+ Assert.Equal(MeterBand.Peak, MeterScale.BandFor(0f));
+
+ // Display fraction: −60 dBFS is empty, 0 dBFS is full, −30 is halfway, and nothing escapes.
+ Assert.Equal(0f, MeterScale.ToFraction(-60f));
+ Assert.Equal(1f, MeterScale.ToFraction(0f));
+ Assert.Equal(0.5f, MeterScale.ToFraction(-30f), 4);
+ Assert.Equal(1f, MeterScale.ToFraction(+12f));
+ Assert.Equal(0f, MeterScale.ToFraction(-120f));
+
+ // Peak hold rises instantly, then decays at the stated rate.
+ Assert.Equal(0.8f, MeterScale.NextPeak(0.2f, 0.8f, 0.05f));
+ Assert.Equal(0.7f, MeterScale.NextPeak(0.8f, 0.1f, 0.1f / 0.35f), 3); // 0.1 fall at 0.35/s
+ Assert.Equal(0f, MeterScale.NextPeak(0.1f, 0f, 10f)); // never negative
+ }
+}