#pragma mode( separator(.,;) integer(h32) )

// ============================================================
// BURR3 - v2.4b
// ============================================================
// Author: Roger Metcalf, 2026.  Written with AI assistance
//   (Anthropic Claude); concept, direction and all hardware
//   testing by the author.
//
// FREE FOR EDUCATIONAL AND PERSONAL USE.  Keep this header with
//   the file and credit the author if you pass it on.  Not for
//   sale or commercial redistribution.  Provided as is, with no
//   warranty: check any result you intend to rely on.
// DISCLAIMER: a teaching and demonstration aid. Not validated for
//   forensic, clinical or legal use; do not rely on its results for
//   legal purposes.
//
// Firmware: this version has NOT yet been run on hardware or on
//   the emulator: v2.4b has run only in the PC harness
//   (ppl_run_v3e).  v2.4 passed the HP Prime Virtual Calculator
//   (build 2026-09-09, 16-17 Oct 2026); v2.4a never ran there.
//   Written for the 2026-09-09 firmware (2.4.2); none of its
//   commands is new in 2.4.x, so older firmware should also work.
//
// What changed in v2.4b (19 Oct 2026; line numbers are v2.4a's)
//   -- B1, B2, B3 and B4 of the fleet review (19 Oct 2026), and the
//   rule Roger set for small values (decision D14 is still open; this
//   is the honest minimum).
//   * B1: the pdf (b3_bpdf, 215-216) divided by (1 + (x/S)^C)^(K+1).
//     For large shapes that power passes the Prime's largest real,
//     1E500, inside the drawn window: Burr3(30, 30, 1, 5) reaches about
//     1E706 at the right edge, Burr3(100, 100, 1, 1) 1E613, and C and K
//     rise without limit by the arrows and drags.  An overflow is a
//     run-time error that opens the Program Editor.  b3_bpdf now works
//     in LOGARITHMS: ln pdf = ln(K C) - ln x + ln z - (K+1) ln(1 + z),
//     z = (x/S)^C, and only the final EXP is taken, so nothing large is
//     ever formed.  Past z = e^100 (about 2.7E43), ln(1 + z) is ln z to
//     far more than 12 digits, and the pdf is K C / (x z^K), again in
//     logs.  New b3_bcdf does the same for the CDF, 1 - (1 + z)^(-K),
//     whose (x/S)^C alone passes 1E500 once C is above about 294 (505,
//     529, 1015).  Below the overflow both give the same numbers as
//     before (checked against scipy.stats.burr12 and 30-digit mpmath).
//     Where v2.4a stopped, the pdf at x0 can now be smaller than the
//     Prime's smallest real and come back as 0; the status row then
//     reads "pdf < 0.0001", as v2.4a already did for an exact-0 CDF
//     (761-762).
//   * B2: mode, median, mean, C, K, S, x0, the moments on the status
//     row and the summary went to STRING(x, 2, d) with no ROUND first,
//     so a value below 10^-d printed the Prime's SCIENTIFIC form: S at
//     its 0.01 floor drew "mode 5.77E-3"; Burr3(1, 100, 1, 1) "median
//     6.96E-3"; Burr3(2, 1, 1, 0.005) "x0 = 5.00E-3" (the E is the
//     Prime's small capital).  New b3_Fx (after b3_Eq) rounds first,
//     and a value that is not 0 but rounds to 0 keeps b3_fmtp's honest
//     "< 0.01" (or "< 0.001") instead of a false 0.00: "mode < 0.01".
//     A mode of 0.0058 now reads "mode 0.01".  In the summary a tiny x0
//     keeps its name inside P( ): "P(X <= x0) < 0.0001 with x0 < 0.01".
//     Sites: 468, 477, 666-683, 744-747, 752, 1019-1021.  K x C (631,
//     776) is 0.5 or more, p (752) 0.005 or more, the tick labels are
//     multiples of their step and b3_fmtp floors its own values: those
//     are left as they were.
//   * B3: the root sign in executable code (476) is now ^0.5 (house
//     rule: no Unicode in code; it also stopped the gate's full parse).
//   * The status row's K x C (780) starts left of x 248 when it is 1,000
//     or more, so it ends by x 320 ("K x C = 56263.62" ran past the edge;
//     the review's own check C7 reaches it once B1 lets such shapes run).
//   * B4: the four MAKELIST caches (427-430) count with the LOCAL b3I,
//     not Home's X (GfxProbe T19: a LOCAL counter works and leaves
//     Home's variables alone; Lessons Learned v7i section 5).
//   * A DISCLAIMER line beside the distribution terms above (Roger,
//     19 Oct 2026): a teaching and demonstration aid, not validated for
//     forensic, clinical or legal use.  A comment only.
//   * The banner, the firmware lines and the help page's title say
//     v2.4b.  Nothing else changed.
//
// What changed in v2.4a (17 Oct 2026, evening; line numbers are v2.4's)
//   -- no "=" before a relation, and no "0.0000" for a P that is not 0.
//   Roger approved the pass, 17 Oct 2026, 21:18.
//   * b3_fmtp writes "< 0.0001" below its floor, and three displays put
//     that after "= ": the status row's "pdf = < 0.0001 (a height), P(X
//     ≤ x₀) = < 0.0001" (726-727), view 4's "its P(X < 0) = < 0.0001"
//     (596) and the summary returned to Home (984-985).  New b3_Eq
//     (after b3_fmtp) returns " = 0.5000", or " < 0.0001" when the
//     relation is already there; the three use it.
//   * P(X ≤ x₀) is 1 - (1 + (x₀/S)^C)^(-K), and for a small x₀ with a
//     large C the power is 1 + a number below the last digit: the
//     difference is exactly 0.  C 25, x₀ 0.3 read "< 0.0001", but
//     x₀ 0.2 read "P(X ≤ x₀) = 0.0000".  x₀ is always above 0 here,
//     where the Burr's CDF is never 0, so an exact 0 now reads
//     "< 0.0001" on the status row and in the summary.  EP_BURR_CDF
//     still holds the computed value.
//   * The help page's title says v2.4a (290).  Nothing else changed.
//
// What changed in v2.4 (16 Oct 2026) -- the first TOUCH version
//   (Fleet Review section 6: Norm3's frame work ported, then the
//   Burr-specific features).  Built on v2.3; every v2.3 fix is kept.
//   Line numbers are v2.3's.
//   * DRAG x0 and TAP THE S-CURVE, as in Norm3 v2.4: the frame loop
//     polls GETKEY and MOUSE together (v2.3 waited on keys, 431); with
//     x0 selected a finger in the plot sets x0 from its pixel column
//     through the drawing's own LFT/W/xmax; a press ON the S-curve (or
//     on the new right-hand 0-1 axis) is the inverse,
//     x0 = S*((1-p)^(-1/K) - 1)^(1/C), p to 0.01 from the finger's
//     height ("p = 0.900 -> x0 = 3.00").  The window is held while a
//     finger is down and refitted on the lift; x0 snaps to a 1-2-5 grid
//     on the lift; every lift is drained with a capped b3_Drain().
//   * RIGHT-HAND CDF AXIS (CDF views): a magenta 0-1 axis with ticks
//     0, .5, 1, and a magenta point riding the S-curve at x0 with a
//     dotted line across to it.  Replaces the lone "1" (350).
//     RGT 314 -> 302 to make room for its labels.
//   * SOFT KEYS {"C","K","S","x0","View","Quit"}: tapping a name SELECTS
//     what the arrows and a horizontal drag adjust; the selected name is
//     highlighted in the title row.  This finally gives S a control.
//     All four arrows adjust the selection (Right/Up up, Left/Down
//     down: C and K by 0.25, S by x1.25, x0 by 1/40 of the window);
//     0 selects the next name; +/- still slide x0; Enter opens a menu
//     (Help, Next view, Quit).  v2.3's Up/Down = K is now "select K,
//     then the arrows" -- noted in the CHANGES file.
//   * DRAG A PARAMETER: with C, K or S selected, a horizontal drag of
//     more than 8 px adjusts it -- C and K by 0.01 a pixel, S by x1.02 a
//     pixel -- with the K*C >= 0.5 floor (v2.3) enforced on every
//     sample.  The x-window is held during the drag (the curve changes
//     shape inside a fixed frame) and refitted on the lift; the y-scale
//     follows.  A TAP (less than 8 px of movement) still sets x0.
//   * THREE MARKERS: mode (orange) S*((C-1)/(K*C+1))^(1/C) for C > 1,
//     else 0; median (magenta) S*(2^(1/K) - 1)^(1/C); mean (green, as
//     before).  Each a dotted vertical with a small label and its value;
//     a marker past the window's edge is named at the right edge with an
//     arrow.  When K*C <= 1 the right 20 % of the plot is hatched light
//     red (a PIXON_P pattern) and "heavy tail: no finite mean" is shown
//     in red on the plot.  Mode, median and the inverse are checked
//     against scipy.stats.burr12 (verify_Burr3.py).
//   * GHOST: after a C, K or S step the previous curve is drawn in light
//     grey under the new one until the next action; during a parameter
//     drag the ghost is the curve the drag started from.
//   * FITTED NORMAL: a fifth view (View cycle: grid+CDF, grid, CDF,
//     plain, grid+normal) draws in grey the normal with the Burr's own
//     mean and sd (cached; the y-scale takes the taller peak) and says
//     what share of that normal lies below 0.  When K*C <= 2 the sd does
//     not exist and the view says so instead of drawing.
//   * VERTICAL ROOM, as in Norm3 v2.4: BOT 194 -> 190, TOPy 22 -> 24,
//     nothing below y = 216 (the soft-key strip paints 218-239).  The
//     legend line (420) is gone; the mu/sigma panel (400-402) is gone
//     (it covered the S-curve where the markers and the point ride):
//     the moments and K*C are on one status row under the x labels,
//     the "window capped at 50xS" notice is on the plot in orange.
//   * The readout says "P(X <= x0)" instead of "CDF" (as Norm3 v2.3).
//   * The cache is filled only when C, K, S or the window changes, so
//     moving x0 costs one pdf and one CDF evaluation a frame.
//   * HELP: a drawn page from Enter -> Help listing every gesture and
//     key, held with b3_WaitAny() (drain, wait for key or tap, drain).
//   * x0 is copied to an ASCII local (b3X0) for the code; the Home
//     signature Burr3(C, K, S, x0) is unchanged.
//   * Ends with RETURN "Burr3: ..." (a summary); EP_BURR_* still set.
// What changed in v2.3 (16 Oct 2026) -- the fleet review's fix list.
//   Line numbers are those of v2.2.
//   * The pdf is evaluated ONCE per frame: the ymax scan (179) now
//     stores its values in a list (b3Pdf) and the shading (235) and the
//     curve (246) read from it.  b3_bpdf calls per frame (two ^ each):
//     up to 871 before, 291 now.  The CDF goes into b3Cdf the same way
//     in the views that draw it (258); its count is unchanged at 291.
//   * The x-window no longer explodes.  At C = K = 0.25 the 0.995
//     quantile (171) is about 6.5E36*S, which gave 37-digit tick labels
//     and a blank plot (183).  Two guards now: the window is capped at
//     50*S, with "window capped at 50xS" on the status line when the cap
//     bites; and the keys will not take K*C below 0.5 (a Left or Down
//     press that would is ignored; the status line shows K*C and the
//     floor).  A Home call with K*C < 0.5 is refused with a message.
//   * A key drain before the frame loop (326): the Enter that closed the
//     launch form could satisfy the first wait and skip a frame.
//   * LOCAL lines 142-145 carried 7 names; split to <= 6.
//   * The bare names x, y, j (75-77), x, z (91-93), x, f (116-118) and
//     i, x, y (143) are now b3V, b3Y, b3J, b3Z, b3F, b3I, b3X (section 5:
//     x is the CAS variable, i the imaginary unit).  The parameters C, K,
//     S and x0 of Burr3() itself are its Home-callable signature,
//     Burr3(C, K, S, x0), and stay as they are.
//   * Panel border (310): RGB(255,165,0) -> the house orange RGB(255,69,0).
//     The panel is wider (168..306) to hold the moment messages below.
//   * A "0" label at the origin of the axes (278).
//   * The three moment cases are named on the panel instead of "undef"
//     (193, 202): "no mean (KxC <= 1)", "no variance (KxC <= 2)".
//   * The CDF "1" label (263) is 4 px lower, clear of the widened panel's
//     top-right corner and of the S-curve's plateau (as in Norm3 v2.3).
//   * To make room for the status line, BOT moved from 200 to 194 and the
//     bottom rows are now 199 (tick labels), 207 (readout), 217 (status)
//     and 228 (legend, the row ChiTable's footer uses on the device).
// v2.2 (10 Oct 2026): every helper routine now carries the program
//   tag "b3_" (fmtp -> b3_fmtp, NiceNum -> b3_NiceNum).
//   The Prime keeps the routines of ALL programs in one shared
//   pool, so two programs that define the same helper name
//   (NiceNum, fmtp and lngamma were each shared by up to eight
//   programs) can run each other's code.  That is what sent
//   StudRange into a Burr3 screen and forced a reset.
//   Rule: every non-EXPORT routine gets the program's tag.
// v2.1 (29 Sep 2026): the key-press variable is renamed from
//   key to kk (house rule: KEY heads user-key definitions).
//   No other change from v2.
// ============================================================

// (C) 2026 Roger D Metcalf
// Program concept and rudimentary code by Roger D. Metcalf
// Code corrected, expanded, and vastly improved by Claude Code Fable 5
// For academic use only – do not rely on results for anything important
// Results have not been rigorously verified and/or validated
// Burr Type XII — interactive pdf/CDF explorer (3-parameter)  — Roger D Metcalf
//   C = shape2   K = shape1   S = scale   x₀ = evaluation point
// Launch:  Burr3(C, K, S, x₀), e.g. Burr3(2, 1, 1, 1).  K×C must be ≥ 0.5.
// TOUCH:    soft keys C K S x₀ choose what the arrows and a drag move;
//           with C, K or S chosen, drag sideways = change it, tap = x₀;
//           with x₀ chosen, drag = x₀;  press on the S-curve (or on the
//           right-hand 0-1 axis) = the inverse, p -> x₀;  View, Quit
// KEYS:     arrows = the chosen quantity    0 = choose the next one
//           (-)/(+) = x₀    Enter = menu (Help, Next view, Quit)
//           Esc = quit
// Live readouts: pdf height and P(X ≤ x₀); mean & sd (when they exist)
// and K×C on the status row; mode, median and mean on the plot.
// Nice-number labeled ticks on both axes; CDF S-curve overlay (0→1
// over full plot height) with its own 0-1 axis on the right.
//
// v2 CHANGES (28 Jul 2026)
//   * Flicker-free: draw to G1, BLIT once per frame. Previously every
//     redraw cleared G0 and rebuilt it in view, so each keypress flashed.
//   * μ/σ panel moved LEFT instead of narrowed. The 206..266 box clipped
//     its own text: " μ = 12.345" starts at x=212 and is ~66 px in font 1,
//     ending near x=278. Now 190..268 with text at 196.
//     (v2.4: the panel is gone; the moments are on the status row.)
//   * All STRING(ROUND(x,d)) -> fixed-decimal STRING(x,2,d). Mode 1 drops
//     trailing zeros regardless of the digit count, so μ = 1.500 printed
//     "1.5" and the axis ticks came out ragged (2 next to 2.5).
//   * pdf/CDF readouts go through b3_fmtp(), which floors at "< 0.0001"
//     rather than printing "0.0000" — in the far tail the density is
//     small, not zero, and this explorer exists to show exactly that.
//   * x-axis tick labels moved BELOW the axis (they were drawn at BOT-11,
//     i.e. inside the plot, on top of the shading and the curve).
//   * Mean marker now dotted, to distinguish it from the solid red x₀ line.
//   * Results exported as EP_* globals, readable in Home after Esc.
//
// MATH VERIFIED (verify_Burr3.py, vs scipy.stats.burr12 with c = C,
//   d = K, scale = S): pdf, CDF, the 0.995-quantile x-range, and the
//   b3_lngamma mean/variance against scipy AND independent quadrature;
//   moment guards keep every b3_lngamma argument positive.  v2.4 adds
//   the mode, the median and the inverse CDF used by the S-curve tap.
//   v2.4b (verify_Burr3_v2.4b.py): the log-space pdf and CDF against
//   burr12 and 30-digit mpmath, tail branch included, up to C, K = 400;
//   no STRING prints the scientific form; check_Burr3.py passes.

EXPORT EP_BURR_MEAN;
EXPORT EP_BURR_SD;
EXPORT EP_BURR_PDF;
EXPORT EP_BURR_CDF;
EXPORT EP_BURR_X0;

// ---- log-gamma (Lanczos): pure numeric, no CAS ----
// b3V is the argument (it was x in v2.2: bare x is the CAS variable).
b3_lngamma(b3V)
BEGIN
  LOCAL b3Y, tmp, ser, b3J, cof;
  cof := {76.18009172947146,-86.50532032941677,24.01409824083091,-1.231739572450155,0.001208650973866179,-0.000005395239384953};
  b3Y := b3V;
  tmp := b3V + 5.5;
  tmp := tmp - (b3V + 0.5)*LN(tmp);
  ser := 1.000000000190015;
  FOR b3J FROM 1 TO 6 DO
    b3Y := b3Y + 1;
    ser := ser + cof(b3J)/b3Y;
  END;
  RETURN -tmp + LN(2.5066282746310005*ser/b3V);
END;

// ---- pdf helper: Burr XII density at b3V for shape C, K and scale S ----
//   pdf = K C z / (x (1 + z)^(K+1)),   z = (x/S)^C
// v2.4b: worked in LOGARITHMS.  For large C and K the power (1 + z)^(K+1)
// passes the Prime's largest real, 1E500, inside the window (Burr3(30,
// 30, 1, 5) needs about 1E706 at the right edge), and an overflow is a
// run-time error.  In logs nothing large is ever formed:
//   ln pdf = ln(K C) - ln x + ln z - (K+1) ln(1 + z),   ln z = C ln(x/S)
// and the final EXP is at most the pdf itself.  (A pdf too small for the
// Prime, below 1E-499, comes back as 0, as before.)
b3_bpdf(b3V,C,K,S)
BEGIN
  LOCAL b3L, b3Z;
  IF b3V <= 0 THEN RETURN 0; END;
  b3L := C*LN(b3V/S);                               // ln z, z = (x/S)^C
  // z above e^100 (about 2.7E43): 1 + z is z to far more than the
  // Prime's 12 digits, so ln(1 + z) = ln z and the pdf is
  // K C z / (x z^(K+1)) = K C / (x z^K); z itself is never formed
  IF b3L > 100 THEN
    RETURN EXP(LN(K*C) - LN(b3V) - K*b3L);
  END;
  b3Z := EXP(b3L);                                  // z, at most 2.7E43 here
  RETURN EXP(LN(K*C) - LN(b3V) + b3L - (K+1)*LN(1+b3Z));
END;

// ---- CDF helper (v2.4b): F(x) = 1 - (1 + z)^(-K), z = (x/S)^C ----
// The same logarithm as b3_bpdf: (x/S)^C alone passes 1E500 once C is
// above about 294 at 50 S.  Past z = e^100, (1 + z)^(-K) is z^(-K) =
// EXP(-K ln z) to the Prime's 12 digits.  Below that it is the v2.4a
// formula with z written EXP(ln z), the same number.
b3_bcdf(b3V,C,K,S)
BEGIN
  LOCAL b3L;
  IF b3V <= 0 THEN RETURN 0; END;
  b3L := C*LN(b3V/S);
  IF b3L > 100 THEN RETURN 1 - EXP(-K*b3L); END;
  RETURN 1 - (1 + EXP(b3L))^(-K);
END;

// ---- fixed-decimal formatter with a floor ----
// Mode 2 keeps trailing zeros. The floor matters in the tail: a pdf of
// 3e-6 printed "0.0000", which reads as zero for a density that is
// merely small. Params pv/dg, not p/d — short parameter names collide
// with builtins. Non-EXPORT so it stays out of the catalog.
b3_fmtp(pv, dg)
BEGIN
  LOCAL thr;
  thr := 10^(-dg);
  IF pv != 0 AND ABS(pv) < thr THEN
    IF pv > 0 THEN RETURN "< " + STRING(thr,2,dg); END;
    RETURN "> -" + STRING(thr,2,dg);
  END;
  RETURN STRING(pv,2,dg);
END;

// " = 0.5000", or " < 0.0001" when b3_fmtp has already written the
// relation (v2.4a): "P(X ≤ x₀) = < 0.0001" said two things at once;
// "P(X ≤ x₀) < 0.0001" says the one that is true.  The first character
// decides, read only when there is one (DIM first: an empty string has
// no first character).
b3_Eq(eqS)
BEGIN
  IF DIM(eqS) > 0 THEN
    IF LEFT(eqS, 1) == "<" OR LEFT(eqS, 1) == ">" THEN RETURN " " + eqS; END;
  END;
  RETURN " = " + eqS;
END;

// ---- a shape, a scale, a marker or x0 to fxD decimals (v2.4b) ----
// STRING(x, 2, d) on the Prime prints the SCIENTIFIC form whenever
// 0 < |x| < 10^-d ("5.77E-3" for a mode of 0.00577 at two decimals;
// Lessons Learned v7i section 3).  Rounded first, a value is either 0
// or at least 10^-d, and the fixed form holds.  But a value that is
// NOT 0 and rounds to 0 would then read 0.00, which is false too, so
// it keeps b3_fmtp's honest "< 0.01" instead.  Exactly 0 (the mode
// when C <= 1) still reads 0.00.
b3_Fx(fxV, fxD)
BEGIN
  LOCAL fxR;
  fxR := ROUND(fxV, fxD);
  // not 0, but it rounds to 0: the floor form ("< 0.01"); b3_fmtp writes
  // it, because |fxV| < 0.5 x 10^-d is under its threshold
  IF fxR == 0 AND fxV <> 0 THEN RETURN b3_fmtp(fxV, fxD); END;
  RETURN STRING(fxR, 2, fxD);
END;

// ---- "nice" number for axis ticks ----
// b3V is the raw step; roundf = 1 rounds to the nearest 1/2/5, 0 rounds up.
b3_NiceNum(b3V, roundf)
BEGIN
  LOCAL expv, b3F, nf;
  IF b3V <= 0 THEN RETURN 1; END;
  expv := FLOOR(LOG(b3V));
  b3F := b3V / 10^expv;
  IF roundf THEN
    CASE
      IF b3F < 1.5 THEN nf := 1; END;
      IF b3F < 3 THEN nf := 2; END;
      IF b3F < 7 THEN nf := 5; END;
      DEFAULT nf := 10;
    END;
  ELSE
    CASE
      IF b3F <= 1 THEN nf := 1; END;
      IF b3F <= 2 THEN nf := 2; END;
      IF b3F <= 5 THEN nf := 5; END;
      DEFAULT nf := 10;
    END;
  END;
  RETURN nf * 10^expv;
END;

// ---- wait for the finger to lift, but never for more than a second ----
// MOUSE(1) is -1 while nothing touches the screen.  Without the time limit
// a stuck or phantom touch would hold the program here for ever, and the
// only way out would be a reset (Lessons Learned section 7; ChiTable's
// csDrain, device-verified).  TICKS is the millisecond clock.
b3_Drain()
BEGIN
  LOCAL drT;
  drT := TICKS;
  REPEAT UNTIL MOUSE(1) == -1 OR TICKS - drT > 1000;
END;

// ---- hold a page until any key OR a tap: drain, wait, drain ----
// The house page-hold (ChiTable's csWaitAny).  A GETKEY drain does not
// drain touch, so both are drained before and after the wait.
b3_WaitAny()
BEGIN
  LOCAL waK, waM, waM1;
  REPEAT waK := GETKEY; UNTIL waK == -1;
  b3_Drain();
  REPEAT
    waK := GETKEY;
    waM := MOUSE;
    waM1 := waM(1);
  UNTIL waK > -1 OR SIZE(waM1) > 0;
  b3_Drain();
  REPEAT waK := GETKEY; UNTIL waK == -1;
END;

// ---- a small label centred on a marker, kept inside the plot ----
// lbS the text, lbX the marker's pixel column, lbY the row, lbC colour.
// Font 1 is about 6 px a character.
b3_Label(lbS, lbX, lbY, lbC)
BEGIN
  LOCAL lbL;
  lbL := lbX - 3*SIZE(lbS);
  lbL := MAX(26, MIN(300 - 6*SIZE(lbS), lbL));
  TEXTOUT_P(lbS, G1, lbL, lbY, 1, lbC);
END;

// ---- the help page: every gesture and key on one drawn page ----
b3_Help()
BEGIN
  LOCAL hpB, hpO, hpG;
  hpB := RGB(0,0,0); hpO := RGB(255,69,0); hpG := RGB(90,90,90);
  RECT_P(G1, 0, 0, 319, 239, RGB(255,255,255));
  RECT_P(G1, 2, 2, 317, 237, hpO, RGB(255,240,220));
  TEXTOUT_P("Burr3 v2.4b  -  touch and keys", G1, 8, 6, 2, hpO);
  TEXTOUT_P("CHOOSE (soft keys C K S x₀, or 0)", G1, 8, 26, 1, hpO);
  TEXTOUT_P("The chosen name is highlighted in the title row.", G1, 8, 38, 1, hpB);
  TEXTOUT_P("Arrows: up or down. C, K by 0.25, S by x1.25.", G1, 8, 50, 1, hpB);
  TEXTOUT_P("TOUCH (in the plot)", G1, 8, 66, 1, hpO);
  TEXTOUT_P("C, K or S chosen: drag sideways to change it", G1, 8, 78, 1, hpB);
  TEXTOUT_P("  (0.01 a pixel; S x1.02); a short tap sets x₀.", G1, 8, 90, 1, hpB);
  TEXTOUT_P("x₀ chosen: drag, and x₀ follows your finger.", G1, 8, 102, 1, hpB);
  TEXTOUT_P("Press ON the magenta S-curve, or on the 0-1 axis", G1, 8, 114, 1, hpB);
  TEXTOUT_P("  at the right: p is that height, x₀ its quantile.", G1, 8, 126, 1, hpB);
  TEXTOUT_P("KEYS  +/- = x₀, Ent = menu, Esc = quit", G1, 8, 142, 1, hpB);
  TEXTOUT_P("READING IT", G1, 8, 158, 1, hpO);
  TEXTOUT_P("Orange = mode, magenta = median, green = mean.", G1, 8, 170, 1, hpB);
  TEXTOUT_P("K×C ≤ 1: hatched tail, no finite mean.", G1, 8, 182, 1, hpB);
  TEXTOUT_P("K×C ≥ 0.5 always. Grey = last curve, or (View 5)", G1, 8, 194, 1, hpG);
  TEXTOUT_P("  the normal with the same mean and sd.", G1, 8, 206, 1, hpG);
  TEXTOUT_P("tap or press any key to return", G1, 8, 222, 1, RGB(30,144,255));
  BLIT_P(G0, 0, 0, G1);
  b3_WaitAny();
END;

EXPORT Burr3(C,K,S,x₀)
BEGIN
  LOCAL LFT,RGT,TOPy,BOT;
  LOCAL W,H,N;
  LOCAL xmax,dx,ymax,dX;
  LOCAL b3I,b3X,b3Y,b3J;
  LOCAL pxx,pyy,ppx,ppy;
  LOCAL x0px,pdf0,cdf0,b3X0;
  LOCAL cSh,cCv,cAx,cX0,cLR;
  LOCAL cTx,cGrid,cMn,cGy,cOrg;
  LOCAL cLO,cGh,cNm;
  LOCAL mmean,mvar,msd,sMu,sSD;
  LOCAL kk,showMode,lastcy,cy;
  LOCAL stp,ystp,nfrac,ynfrac;
  LOCAL tickx,ticky,xv,yv;
  LOCAL b3Pdf,b3Cdf,b3Nrm,b3Gh;
  LOCAL b3Q,b3Cap,b3New,sKC,b3Wn;
  LOCAL b3M,b3M1,b3Ev,b3Act,b3Quit;
  LOCAL b3Drag,b3TX,b3TY,b3LX,b3LY;
  LOCAL b3PX,b3PY,b3P0,b3P,b3Msg;
  LOCAL b3Re,b3CdfOK,b3NrmOK,b3Frz,b3Snap;
  LOCAL b3Sel,b3GhOn,b3GhDx,b3C,b3Lbl;
  LOCAL b3Mode,b3Med,b3Pk,b3Tap,b3Hx;
  LOCAL b3XS,b3XT;

  IF C  <= 0 THEN RETURN "C must be > 0";  END;
  IF K  <= 0 THEN RETURN "K must be > 0";  END;
  IF S  <= 0 THEN RETURN "S must be > 0";  END;
  IF x₀ <= 0 THEN RETURN "x₀ must be > 0"; END;
  // Below K×C = 0.5 the tail is so heavy that the 0.995 quantile runs to
  // thousands of S and beyond (6.5E36*S at C = K = 0.25): no window can
  // show it.  The keys and the drags keep K×C >= 0.5 too.
  IF K*C < 0.5 THEN RETURN "K×C must be ≥ 0.5 (a heavier tail has no usable window)"; END;
  // x₀ arrives as the fourth argument; the program works on an ASCII copy,
  // b3X0 (house rule: no new Unicode in executable code).
  b3X0 := x₀;

  // ---- constants (screen 320x240, origin top-left, y increases DOWN) ----
  // Rows ABOVE the plot: 1 = title (C, K, S, x₀; the chosen one boxed),
  // 12 = the pdf / P(X <= x₀) readout.  The plot starts at TOPy = 24.
  // Rows BELOW it: x tick labels at BOT+4 = 194, the status row (moments,
  // K×C) at BOT+15 = 205, ending by 215.  Nothing is drawn below 216:
  // the soft-key strip DRAWMENU paints is 218..239.
  // RGT is 302 (was 314) so the right-hand CDF axis labels fit.
  // THE TOUCH HANDLER USES THESE SAME CONSTANTS: a finger at pixel column
  // px means x = (px - LFT)/W*xmax, the drawing's own map.
  LFT:= 24; RGT:=302; TOPy:=24; BOT:=190;
  W:=RGT-LFT; H:=BOT-TOPy; N:=W;
  // palette (Lessons Learned section 13), set once, not inside loops
  cSh:=RGB(170,205,255); cCv:=RGB(0,60,200); cAx:=RGB(0,0,0);
  cX0:=RGB(255,0,0); cTx:=RGB(200,0,200); cGrid:=RGB(225,228,235);
  cMn:=RGB(0,140,60); cGy:=RGB(90,90,90); cOrg:=RGB(255,69,0);
  cLR:=RGB(255,235,235); cLO:=RGB(255,240,220);
  cGh:=RGB(200,200,200);            // the ghost: light grey
  cNm:=RGB(90,90,90);               // the fitted normal: grey
  // 0 = grid+CDF, 1 = grid, 2 = CDF, 3 = plain, 4 = grid + fitted normal
  showMode := 0;
  mmean := 0; mvar := 0; msd := 0;
  b3Cap := 0;

  // ---- the state of the touch loop ----
  // b3Sel: the quantity the arrows and a drag adjust -- 1 C, 2 K, 3 S,
  //   4 x₀ (the soft keys 1-4 choose it).  C to start, so Left/Right do
  //   what they did in v2.3.
  // b3Drag: what the finger holds -- 0 nothing, 1 x₀, 2 the S-curve
  //   (inverse), 4 a press with C/K/S chosen that has not moved 8 px yet
  //   (a tap so far), 5 a parameter drag.
  // b3Frz = 1: the x-window is held (a finger is down).
  // b3Re = 1: refill the pdf cache.  b3CdfOK, b3NrmOK: the CDF and the
  //   fitted-normal caches match it.
  // b3GhOn = 1: draw the ghost (the curve before the last step).
  // b3Msg: row 2 shows 0 the readout, 1 the inverse, 2 a refused step.
  b3Sel := 1; b3Drag := 0; b3Frz := 0; b3Msg := 0; b3Act := 0; b3Quit := 0;
  b3Re := 1; b3CdfOK := 0; b3NrmOK := 0; b3GhOn := 0; b3GhDx := 1;
  b3LX := -1; b3LY := -1; b3PX := 0; b3PY := 0; b3P0 := 1; b3P := 0.5;
  b3Tap := 0; b3Pk := 1; xmax := 0; ymax := 1; pdf0 := 0; cdf0 := 0;

  // ---- caches, sized once: b3Pdf(k), b3Cdf(k), b3Nrm(k) hold the pdf,
  //      the CDF and the fitted normal at pixel column k (x = k*dx),
  //      k = 1..N; b3Gh is the ghost (a copy of b3Pdf before a step).
  //      v2.4b: the MAKELIST counter is this routine's own LOCAL b3I.
  //      v2.4a counted with Home's X (it could not be K: K is this
  //      program's shape parameter, and MAKELIST writes its counter),
  //      which overwrote X in Home.  GfxProbe T19 (emulator, 17 Oct
  //      2026) showed a LOCAL counter works and leaves Home's variables
  //      alone; Lessons Learned v7i section 5: new code uses one. ----
  b3Pdf := MAKELIST(0, b3I, 1, N, 1);
  b3Cdf := MAKELIST(0, b3I, 1, N, 1);
  b3Nrm := MAKELIST(0, b3I, 1, N, 1);
  b3Gh  := MAKELIST(0, b3I, 1, N, 1);

  DIMGROB_P(G1,320,240);            // off-screen buffer, built once

  // ---- drain the key that closed the launch form, and any touch, so
  //      neither can satisfy the first wait below (section 7) ----
  REPEAT kk := GETKEY; UNTIL kk == -1;
  b3_Drain();

  REPEAT
    // ==== 1. the x-range: the 0.995 quantile, capped, x₀ kept on screen ====
    // The quantile S*((0.005)^(-1/K)-1)^(1/C) grows fast as K×C shrinks:
    // 199*S at C = K = 1, about 1.6E9*S at C = K = 0.5, about 6.5E36*S at
    // C = K = 0.25 -- v2.2 then printed 37-digit tick labels and a blank
    // plot.  The window is capped at 50*S; when the cap bites the plot
    // says so (section 6: say on screen what was suppressed).
    // While a finger is down (b3Frz = 1) the window is HELD, so the map
    // from the finger's pixel to x cannot move under it; it is refitted
    // on the lift (BRIEF_TOUCH: freeze during a drag, refit on lift).
    IF b3Frz == 0 THEN
      b3Q  := S*((0.005)^(-1/K)-1)^(1/C);
      b3Wn := MIN(b3Q, 50*S);
      b3Wn := MAX(b3Wn, b3X0*1.15);
      IF b3Wn < b3Q THEN b3Cap := 1; ELSE b3Cap := 0; END;
      IF b3Wn <> xmax THEN xmax := b3Wn; b3Re := 1; END;
    END;
    dx   := xmax/N;
    dX   := xmax/40;                  // x₀ slide step
    // the grid x₀ snaps to on lift: 1/100 of the window, 1-2-5
    b3Snap := b3_NiceNum(xmax/100, 1);

    // ==== 2. moments (guards: mean K·C>1, variance K·C>2) ====
    // The order matters: K·C>2 implies K·C>1, so mmean is always defined
    // before the variance line uses it.  "No mean" is a teaching feature
    // of this distribution, not a failure.  (Before the caches now: the
    // fitted normal is built from these.)
    // v2.4b: the moments through b3_Fx (rounded first: never the
    // scientific form) and b3_Eq ("μ < 0.001" for a tiny scale)
    IF K*C > 1 THEN
      mmean := S*EXP(b3_lngamma(1+1/C)+b3_lngamma(K-1/C)-b3_lngamma(K));
      sMu := "μ" + b3_Eq(b3_Fx(mmean,3));
    ELSE
      mmean := 0;
      sMu := "no mean (K×C ≤ 1)";
    END;
    IF K*C > 2 THEN
      mvar := S^2*EXP(b3_lngamma(1+2/C)+b3_lngamma(K-2/C)-b3_lngamma(K)) - mmean^2;
      IF mvar < 0 THEN mvar := 0; END;
      // v2.4b: the square root as a power (house rule: no root sign in code)
      msd := mvar^0.5;
      sSD := "σ" + b3_Eq(b3_Fx(msd,3));
    ELSE
      mvar := 0; msd := 0;
      sSD := "no variance (K×C ≤ 2)";
    END;
    // mode and median, closed forms (verify_Burr3.py vs scipy.stats.burr12):
    // the mode is where the pdf turns, x^C = (C-1)/(K*C+1), which needs
    // C > 1 (for C <= 1 the pdf falls from x = 0, so the mode is 0); the
    // median solves F(x) = 1/2.
    IF C > 1 THEN b3Mode := S*((C-1)/(K*C+1))^(1/C); ELSE b3Mode := 0; END;
    b3Med := S*(2^(1/K)-1)^(1/C);

    // ==== 3. the caches: refilled only when C, K, S or the window moved ====
    // The scan stores each value in b3Pdf and keeps the largest (b3Pk),
    // the Burr's peak on screen, for the y-scale (section 6: cache the
    // density).  Moving x₀ needs no refill.
    IF b3Re == 1 THEN
      b3Pk := 0;
      FOR b3I FROM 1 TO N DO
        b3Y := b3_bpdf(b3I*dx,C,K,S);
        b3Pdf(b3I) := b3Y;
        IF b3Y > b3Pk THEN b3Pk := b3Y; END;
      END;
      b3Re := 0; b3CdfOK := 0; b3NrmOK := 0;
    END;
    // the CDF, filled only in the views that draw the S-curve
    // (v2.4b: through b3_bcdf, in logs, so (x/S)^C cannot overflow)
    IF (showMode == 0 OR showMode == 2) AND b3CdfOK == 0 THEN
      FOR b3I FROM 1 TO N DO
        b3Cdf(b3I) := b3_bcdf(b3I*dx,C,K,S);
      END;
      b3CdfOK := 1;
    END;
    // the fitted normal (view 4), only when the sd exists (K×C > 2)
    IF showMode == 4 AND K*C > 2 AND b3NrmOK == 0 THEN
      FOR b3I FROM 1 TO N DO
        b3Nrm(b3I) := NORMALD(mmean, msd, b3I*dx);
      END;
      b3NrmOK := 1;
    END;
    // y-range: the Burr's peak on screen, or the fitted normal's peak
    // when that is taller (check BOTH curves before drawing a reference:
    // Lessons Learned section 6), plus 15 %.  Not frozen: it follows C,
    // K and S, also during a drag.
    ymax := b3Pk;
    IF showMode == 4 AND K*C > 2 THEN
      ymax := MAX(ymax, NORMALD(mmean, msd, mmean));
    END;
    ymax := ymax*1.15;
    IF ymax <= 0 THEN ymax := 1; END;

    // ==== 4. the values at x₀ ====
    pdf0 := b3_bpdf(b3X0,C,K,S);
    cdf0 := b3_bcdf(b3X0,C,K,S);      // v2.4b: in logs (b3_bcdf)
    x0px := IP(LFT + b3X0/xmax*W);

    RECT_P(G1,0,0,319,239,RGB(255,255,255));      // clear the buffer

    // ---- nice-number tick positions ----
    stp := b3_NiceNum(xmax/6, 1);
    nfrac := MAX(0, -FLOOR(LOG(stp)));
    ystp := b3_NiceNum(ymax/4, 1);
    ynfrac := MAX(0, -FLOOR(LOG(ystp)));

    // ---- grid at nice values (behind everything) ----
    IF showMode == 0 OR showMode == 1 OR showMode == 4 THEN
      xv := stp;
      WHILE xv <= xmax + stp/1000 DO
        tickx := LFT + IP(xv/xmax*W);
        IF tickx > LFT AND tickx <= RGT THEN
          LINE_P(G1, tickx, TOPy, tickx, BOT-1, cGrid);
        END;
        xv := xv + stp;
      END;
      yv := ystp;
      WHILE yv < ymax DO
        ticky := BOT - IP(yv/ymax*H);
        LINE_P(G1, LFT+1, ticky, RGT, ticky, cGrid);
        yv := yv + ystp;
      END;
    END;

    // ---- heavy tail (K×C <= 1): hatch the right 20 % of the plot ----
    // A light-red band with a sparse diagonal dot pattern (PIXON_P: there
    // is no fill pattern on the Prime): the tail goes on past the window
    // so slowly that the mean is infinite.  b3Hx is the band's left edge.
    // Rows 4 px apart, dots 8 px apart, shifted 2 px a row: ~290 dots.
    IF K*C <= 1 THEN
      b3Hx := LFT + IP(0.8*W);
      RECT_P(G1, b3Hx, TOPy, RGT, BOT-1, cLR);
      FOR b3I FROM TOPy+2 TO BOT-2 STEP 4 DO
        b3J := ((b3I - TOPy)/2) MOD 8;
        FOR pxx FROM b3Hx + b3J TO RGT-1 STEP 8 DO
          PIXON_P(G1, pxx, b3I, cX0);
        END;
      END;
    END;

    // ---- shading: iterate PIXEL COLUMNS (no gaps), x<=x₀ ----
    // Column pxx is cache entry pxx - LFT (1..N): the same x as the curve.
    FOR pxx FROM LFT+1 TO RGT DO
      b3X := (pxx - LFT)/W*xmax;
      IF b3X <= b3X0 THEN
        b3Y := b3Pdf(pxx - LFT);
        pyy := IP(BOT - b3Y/ymax*H);
        IF pyy < TOPy THEN pyy := TOPy; END;
        LINE_P(G1, pxx, BOT, pxx, pyy, cSh);
      END;
    END;

    // ---- the ghost: the curve before the last C, K or S step, in light
    //      grey, drawn BEFORE the new curve.  Its columns are those of
    //      the window it was drawn in (b3GhDx), mapped into this one. ----
    IF b3GhOn == 1 THEN
      ppx := -1;
      FOR b3I FROM 1 TO N DO
        pxx := IP(LFT + b3I*b3GhDx/xmax*W);
        IF pxx <= RGT THEN
          pyy := IP(BOT - b3Gh(b3I)/ymax*H);
          IF pyy < TOPy THEN pyy := TOPy; END;
          IF ppx >= 0 THEN LINE_P(G1, ppx, ppy, pxx, pyy, cGh); END;
          ppx := pxx; ppy := pyy;
        END;
      END;
    END;

    // ---- pdf curve on top (from the cache) ----
    ppx := -1;
    FOR b3I FROM 1 TO N DO
      pxx := LFT + b3I;
      pyy := IP(BOT - b3Pdf(b3I)/ymax*H);
      IF pyy < TOPy THEN pyy := TOPy; END;
      IF ppx >= 0 THEN LINE_P(G1, ppx, ppy, pxx, pyy, cCv); END;
      ppx := pxx; ppy := pyy;
    END;

    // ---- view 4: the fitted normal, same mean and sd, in grey ----
    // Skewed against symmetric with the same μ and σ; the normal also
    // puts probability below 0, where the Burr has none -- said on screen.
    IF showMode == 4 THEN
      IF K*C > 2 THEN
        ppx := -1;
        FOR b3I FROM 1 TO N DO
          pxx := LFT + b3I;
          pyy := IP(BOT - b3Nrm(b3I)/ymax*H);
          IF pyy < TOPy THEN pyy := TOPy; END;
          IF ppx >= 0 THEN LINE_P(G1, ppx, ppy, pxx, pyy, cNm); END;
          ppx := pxx; ppy := pyy;
        END;
        // (rows TOPy+38 and +48: below the three marker labels)
        TEXTOUT_P("grey = normal, same μ and σ", G1, RGT-168, TOPy+38, 1, cNm);
        // v2.4a: b3_Eq -- "its P(X < 0) < 0.0001", not "= < 0.0001"
        TEXTOUT_P("its P(X < 0)"+b3_Eq(b3_fmtp(NORMALD_CDF(mmean, msd, 0),4)),
                  G1, RGT-168, TOPy+48, 1, cNm);
      ELSE
        TEXTOUT_P("normal fit needs K×C > 2 (now "+STRING(K*C,2,2)+")",
                  G1, RGT-246, TOPy+38, 1, cOrg);
      END;
    END;

    // ---- CDF S-curve (0..1 over the full plot height) and its axis ----
    IF showMode == 0 OR showMode == 2 THEN
      lastcy := -1;
      FOR b3I FROM 1 TO N DO
        cy := IP(BOT - b3Cdf(b3I)*H);
        pxx := LFT + b3I;
        IF lastcy >= 0 THEN LINE_P(G1, pxx-1, lastcy, pxx, cy, cTx); END;
        lastcy := cy;
      END;
      // the right-hand 0-1 axis for the S-curve: a magenta line at RGT
      // with ticks at 0, .5 and 1 (it replaces v2.3's lone "1")
      LINE_P(G1, RGT, TOPy, RGT, BOT, cTx);
      FOR b3I FROM 0 TO 2 DO
        ticky := BOT - IP(b3I*H/2);
        LINE_P(G1, RGT, ticky, RGT+3, ticky, cTx);
      END;
      TEXTOUT_P("1", G1, RGT+6, TOPy-2, 1, cTx);
      TEXTOUT_P(".5", G1, RGT+5, BOT-IP(H/2)-4, 1, cTx);
      TEXTOUT_P("0", G1, RGT+6, BOT-9, 1, cTx);
    END;

    // ---- three markers: mode (orange), median (magenta), mean (green) ----
    // Dotted verticals, each with its name and value on its own row at
    // the top so that close markers do not print over each other.  A
    // marker beyond the window is named at the right edge with an arrow.
    // As C falls (drag it) the three slide apart: the longer the right
    // tail, the further the mean is pulled past the median and the mode.
    // v2.4b: each value through b3_Fx -- rounded first, so "mode 0.01"
    // where v2.4a printed "mode 5.77E-3", and "mode < 0.01" for a mode
    // that is not 0 but rounds to 0.00 (an exact 0 still reads 0.00)
    tickx := LFT + IP(b3Mode/xmax*W);
    IF tickx <= RGT THEN
      FOR b3I FROM TOPy+11 TO BOT STEP 4 DO PIXON_P(G1, MAX(LFT+1, tickx), b3I, cOrg); END;
      b3_Label("mode "+b3_Fx(b3Mode,2), tickx, TOPy+1, cOrg);
    ELSE
      b3_Label("mode "+b3_Fx(b3Mode,2)+" "+CHAR(8594), 400, TOPy+1, cOrg);
    END;
    tickx := LFT + IP(b3Med/xmax*W);
    IF tickx <= RGT THEN
      FOR b3I FROM TOPy+22 TO BOT STEP 4 DO PIXON_P(G1, tickx, b3I, cTx); END;
      b3_Label("median "+b3_Fx(b3Med,2), tickx, TOPy+12, cTx);
    ELSE
      b3_Label("median "+b3_Fx(b3Med,2)+" "+CHAR(8594), 400, TOPy+12, cTx);
    END;
    IF K*C > 1 THEN
      tickx := LFT + IP(mmean/xmax*W);
      IF tickx <= RGT THEN
        FOR b3I FROM TOPy+33 TO BOT STEP 4 DO PIXON_P(G1, tickx, b3I, cMn); END;
        b3_Label("mean "+b3_Fx(mmean,2), tickx, TOPy+23, cMn);
      ELSE
        b3_Label("mean "+b3_Fx(mmean,2)+" "+CHAR(8594), 400, TOPy+23, cMn);
      END;
    END;

    // ---- axes + labeled nice ticks ----
    LINE_P(G1, LFT, BOT, RGT, BOT, cAx);
    LINE_P(G1, LFT, BOT, LFT, TOPy, cAx);
    // "0" at the origin: the x-axis starts at 0 (the Burr has no mass
    // below it) and the first labeled tick is one step to the right.
    TEXTOUT_P("0", G1, LFT-3, BOT+4, 1, cGy);
    xv := stp;
    WHILE xv <= xmax + stp/1000 DO
      tickx := LFT + IP(xv/xmax*W);
      IF tickx <= RGT THEN
        LINE_P(G1, tickx, BOT, tickx, BOT+3, cAx);
        // BELOW the axis, centred under the tick (3*SIZE is half the
        // label's width in font 1, as Norm3 v2.3 does).
        b3Lbl := STRING(xv,2,nfrac);
        TEXTOUT_P(b3Lbl, G1, tickx - 3*SIZE(b3Lbl), BOT+4, 1, cGy);
      END;
      xv := xv + stp;
    END;
    yv := ystp;
    WHILE yv < ymax DO
      ticky := BOT - IP(yv/ymax*H);
      LINE_P(G1, LFT-3, ticky, LFT, ticky, cAx);
      TEXTOUT_P(STRING(yv,2,ynfrac), G1, 1, ticky-4, 1, cGy);
      yv := yv + ystp;
    END;

    // ---- x₀ marker (vertical) + dotted horizontal to the y-axis ----
    pyy  := IP(BOT - pdf0/ymax*H);
    IF pyy < TOPy THEN pyy := TOPy; END;
    LINE_P(G1, x0px, BOT, x0px, pyy, cX0);
    FOR b3I FROM LFT+1 TO x0px STEP 4 DO
      PIXON_P(G1, b3I, pyy, cX0);
    END;
    // ---- the magenta point riding the S-curve at x₀, with a dotted
    //      line across to the right-hand axis ----
    IF showMode == 0 OR showMode == 2 THEN
      cy := IP(BOT - cdf0*H);
      FOR b3I FROM x0px+4 TO RGT STEP 4 DO
        PIXON_P(G1, b3I, cy, cTx);
      END;
      RECT_P(G1, x0px-2, cy-2, x0px+2, cy+2, cTx);
    END;

    // ---- said on the plot: no finite mean, and a capped window ----
    IF K*C <= 1 THEN
      RECT_P(G1, RGT-164, BOT-45, RGT-2, BOT-32, cX0, RGB(255,255,255));
      TEXTOUT_P("heavy tail: no finite mean", G1, RGT-160, BOT-43, 1, cX0);
    END;
    IF b3Cap == 1 THEN
      RECT_P(G1, RGT-134, BOT-29, RGT-2, BOT-16, cOrg, RGB(255,255,255));
      TEXTOUT_P("window capped at 50×S", G1, RGT-130, BOT-27, 1, cOrg);
    END;

    // ---- row 1: the title, the four quantities, the chosen one boxed ----
    TEXTOUT_P("Burr", G1, 2, 1, 1, cAx);
    b3I := 34 + (b3Sel - 1)*68;
    RECT_P(G1, b3I-3, 0, b3I+65, 11, cOrg, cLO);
    // v2.4b: b3_Fx and b3_Eq -- "S = 0.01", "x₀ < 0.01" for a value
    // too small for two decimals, never the scientific form
    TEXTOUT_P("C"+b3_Eq(b3_Fx(C,2)), G1, 34, 1, 1, cAx);
    TEXTOUT_P("K"+b3_Eq(b3_Fx(K,2)), G1, 102, 1, 1, cAx);
    TEXTOUT_P("S"+b3_Eq(b3_Fx(S,2)), G1, 170, 1, 1, cAx);
    TEXTOUT_P("x₀"+b3_Eq(b3_Fx(b3X0,2)), G1, 238, 1, 1, cAx);
    // ---- row 2: the readout, the inverse, or a refused step ----
    // pdf is the curve's HEIGHT at x₀ (not a probability); P(X <= x₀)
    // is the shaded AREA to the left of x₀.
    // (v2.4b: x₀ through b3_Fx; p needs no ROUND, its clamp keeps it at
    // 0.005 or more)
    IF b3Msg == 1 THEN
      TEXTOUT_P("p = "+STRING(b3P,2,3)+" "+CHAR(8594)+" x₀"+b3_Eq(b3_Fx(b3X0,2)),
                G1, 4, 12, 1, cTx);
    ELSE
      IF b3Msg == 2 THEN
        TEXTOUT_P("step refused: K×C must stay ≥ 0.5", G1, 4, 12, 1, cOrg);
      ELSE
        // v2.4a: b3_Eq on both numbers; and P(X ≤ x₀) is never 0 for an
        // x₀ above 0, so an exact 0 (1 - (1 + u)^(-K) with u below the last
        // digit: a small x₀ and a large C) reads "< 0.0001", not "0.0000".
        // v2.4b: the same for the pdf, which is never 0 above 0 either: far
        // in the tail of a large-shape Burr (Burr3(30, 30, 1, 5): about
        // 1E-628 at x₀) the log-space pdf comes back as 0, below the
        // Prime's smallest real, and reads "< 0.0001" (v2.4a stopped there)
        TEXTOUT_P("pdf"+b3_Eq(IFTE(pdf0 > 0, b3_fmtp(pdf0,4), "< 0.0001"))+" (a height), P(X ≤ x₀)"+
                  b3_Eq(IFTE(cdf0 > 0, b3_fmtp(cdf0,4), "< 0.0001")), G1, 4, 12, 1, cTx);
      END;
    END;
    // ---- the status row: the moments (they may not exist) and K×C,
    //      orange when it sits on the 0.5 floor ----
    IF K*C > 2 THEN
      TEXTOUT_P(sMu+", "+sSD, G1, 4, BOT+15, 1, cAx);
    ELSE
      IF K*C > 1 THEN
        TEXTOUT_P(sMu+", "+sSD, G1, 4, BOT+15, 1, cAx);
      ELSE
        TEXTOUT_P(sMu, G1, 4, BOT+15, 1, cX0);
      END;
    END;
    sKC := "K×C = " + STRING(K*C,2,2);
    IF ROUND(K*C, 6) <= 0.5 THEN
      TEXTOUT_P(sKC+" (floor)", G1, 208, BOT+15, 1, cOrg);
    ELSE
      // v2.4b: a K×C of 1,000 or more ("K×C = 56263.62", 14 characters)
      // ran past the right edge from x 248.  The text now ends by x 320 at
      // 6 px a character; every K×C below 1,000 still starts at 248.  (Such
      // shapes mostly stopped v2.4a in b3_bpdf first: B1.)
      TEXTOUT_P(sKC, G1, MIN(248, 320 - 6*SIZE(sKC)), BOT+15, 1, cGy);
    END;

    BLIT_P(G0, 0, 0, G1);             // one flicker-free page flip
    // the soft keys, painted on the screen after the blit, every frame
    DRAWMENU({"C", "K", "S", "x₀", "View", "Quit"});

    // ==== 5. input: a key, a press, a drag sample, or a lift ====
    // b3Ev: 1 a press or a moved finger, 2 the finger lifted, 3 a key.
    // While something is held the loop waits until the finger MOVES or
    // lifts (a resting finger redraws nothing).  A key ends a hold.
    b3Ev := 0; kk := -1;
    REPEAT
      kk := GETKEY;
      b3M := MOUSE;
      b3M1 := b3M(1);
      IF b3Drag > 0 THEN
        IF SIZE(b3M1) == 0 THEN
          b3Ev := 2;
        ELSE
          IF b3M1(1) <> b3LX OR b3M1(2) <> b3LY THEN b3Ev := 1; END;
        END;
      ELSE
        IF SIZE(b3M1) > 0 THEN b3Ev := 1; END;
      END;
      IF kk > -1 THEN b3Ev := 3; END;
    UNTIL b3Ev > 0;
    // The ghost stays on screen until the next action; it is kept
    // through a parameter drag (it is the curve the drag started from).
    IF b3Drag < 4 THEN b3GhOn := 0; END;

    // ---- the finger lifted (or a key ended the hold) ----
    b3Tap := 0;
    IF b3Ev == 2 OR (b3Ev == 3 AND b3Drag > 0) THEN
      IF b3Drag == 4 THEN
        // C, K or S was chosen but the finger never moved 8 px: a TAP.
        // It sets x₀ (or, on the S-curve, the inverse) at the press
        // point, exactly as when x₀ is chosen -- handled just below.
        b3Tap := 1; b3TX := b3PX; b3TY := b3PY;
      END;
      IF b3Drag == 1 THEN
        // x₀ snaps to the grid, staying inside the window and above 0
        b3X0 := MAX(b3Snap, ROUND(b3X0/b3Snap, 0)*b3Snap);
        IF b3X0 > xmax THEN b3X0 := b3X0 - b3Snap; END;
      END;
      b3Drag := 0; b3Frz := 0;
      b3_Drain();
    END;

    // ---- a press, a drag sample, or the tap just completed ----
    IF b3Ev == 1 OR b3Tap == 1 THEN
      IF b3Ev == 1 THEN b3TX := b3M1(1); b3TY := b3M1(2); END;
      IF b3Drag == 0 THEN
        // A NEW PRESS: decide what it holds, once, from the first sample.
        IF b3TY >= 218 THEN
          // the soft-key strip: six 53-px slots; act once, on the lift
          b3_Drain();
          b3Act := MIN(6, IP(b3TX/53.4) + 1);
        ELSE
          IF b3TX >= LFT AND b3TX <= RGT AND b3TY >= TOPy AND b3TY <= BOT THEN
            b3PX := b3TX; b3PY := b3TY;
            IF b3Sel == 4 OR b3Tap == 1 THEN
              // x₀ follows the finger -- unless the press is ON the
              // S-curve (within 16 px, and nearer it than the pdf)
              b3Drag := 1;
              IF showMode == 0 OR showMode == 2 THEN
                b3J := MAX(1, MIN(N, b3TX - LFT));
                b3Y := ABS(b3TY - (BOT - b3Cdf(b3J)*H));
                IF b3Y <= 16 AND b3Y < ABS(b3TY - (BOT - b3Pdf(b3J)/ymax*H)) THEN
                  b3Drag := 2;
                END;
              END;
            ELSE
              // C, K or S chosen: a tap or a drag?  Wait and see (4);
              // keep the curve as it is now, for the ghost
              b3Drag := 4;
              IF b3Sel == 1 THEN b3P0 := C; END;
              IF b3Sel == 2 THEN b3P0 := K; END;
              IF b3Sel == 3 THEN b3P0 := S; END;
              b3Gh := b3Pdf; b3GhDx := dx;
            END;
          ELSE
            // on the right-hand 0-1 axis: the inverse as well
            IF (showMode == 0 OR showMode == 2) AND b3TX > RGT
               AND b3TY >= TOPy AND b3TY <= BOT THEN
              b3Drag := 2;
            ELSE
              b3_Drain();             // outside the plot: nothing to do
            END;
          END;
        END;
      END;
      // APPLY THIS SAMPLE to what is held (the first sample too); the
      // finger is clamped to the plot box.
      IF b3Drag > 0 THEN
        IF b3Tap == 0 THEN b3LX := b3TX; b3LY := b3TY; END;
        b3TX := MAX(LFT+1, MIN(RGT, b3TX));
        b3TY := MAX(TOPy, MIN(BOT, b3TY));
        b3Frz := 1;                   // hold the window while the finger is down
        IF b3Drag == 1 THEN
          // x₀ from the pixel column: the drawing's own map
          b3X0 := (b3TX - LFT)/W*xmax;
          b3Msg := 0;
        END;
        IF b3Drag == 2 THEN
          // p from the height on the right-hand 0-1 axis, rounded to 0.01
          // (one pixel is 1/H = 0.006 of p); 0.025 and 0.975 are caught
          // within 0.003.  p is kept inside the part of the S-curve on
          // screen, then x₀ = S*((1-p)^(-1/K) - 1)^(1/C), the Burr XII
          // inverse CDF (verify_Burr3.py).
          b3P := (BOT - b3TY)/H;
          IF ABS(b3P - 0.975) < 0.003 THEN
            b3P := 0.975;
          ELSE
            IF ABS(b3P - 0.025) < 0.003 THEN
              b3P := 0.025;
            ELSE
              b3P := ROUND(b3P*100, 0)/100;
            END;
          END;
          b3P := MAX(b3P, MAX(0.005, b3Cdf(2)));
          b3P := MIN(b3P, MIN(0.995, b3Cdf(N-1)));
          IF b3P > 0 AND b3P < 1 THEN
            b3X0 := S*((1-b3P)^(-1/K)-1)^(1/C);
            b3Msg := 1;
          END;
        END;
        IF b3Drag == 4 THEN
          // more than 8 px sideways: it is a drag of the chosen parameter
          IF ABS(b3TX - b3PX) > 8 THEN b3Drag := 5; b3GhOn := 1; END;
        END;
        IF b3Drag == 5 THEN
          // C and K: 0.01 a pixel; S: x1.02 a pixel.  The K×C >= 0.5
          // floor holds on every sample: C or K stops at the smallest
          // hundredth that keeps the product at or above 0.5.
          IF b3Sel == 1 THEN
            C := ROUND(b3P0 + 0.01*(b3TX - b3PX), 2);
            IF C*K < 0.5 THEN C := CEILING(50/K)/100; END;
          END;
          IF b3Sel == 2 THEN
            K := ROUND(b3P0 + 0.01*(b3TX - b3PX), 2);
            IF K*C < 0.5 THEN K := CEILING(50/C)/100; END;
          END;
          IF b3Sel == 3 THEN
            S := MAX(0.01, b3P0*1.02^(b3TX - b3PX));
          END;
          b3Re := 1; b3Msg := 0;
        END;
      END;
      IF b3Tap == 1 THEN
        // the tap is over: snap as for a lifted x₀ drag, release the window
        IF b3Drag == 1 THEN
          b3X0 := MAX(b3Snap, ROUND(b3X0/b3Snap, 0)*b3Snap);
          IF b3X0 > xmax THEN b3X0 := b3X0 - b3Snap; END;
        END;
        b3Drag := 0; b3Frz := 0;
      END;
    END;

    // ---- keys: the hardware keys stay as shortcuts for the strip ----
    // b3Act: 1-4 choose C, K, S, x₀; 5 view; 6 quit (the six soft keys);
    //        7 the chosen quantity down, 8 up; 9 x₀-, 10 x₀+;
    //        11 choose the next name; 12 menu
    IF b3Ev == 3 THEN
      IF kk == 7 OR kk == 12 THEN b3Act := 7;  END;  // Left / Down : down
      IF kk == 8 OR kk == 2  THEN b3Act := 8;  END;  // Right / Up  : up
      IF kk == 4  THEN b3Act := 6;  END;             // Esc   : quit
      IF kk == 45 THEN b3Act := 9;  END;             // (-)   : x₀ down
      IF kk == 50 THEN b3Act := 10; END;             // (+)   : x₀ up
      IF kk == 42 THEN b3Act := 11; END;             // 0     : next name
      IF kk == 30 THEN b3Act := 12; END;             // Enter : menu
    END;

    // ---- the menu (Enter): Help, Next view, Quit ----
    IF b3Act == 12 THEN
      b3Act := 0;
      b3C := 0;
      CHOOSE(b3C, "Burr3", {"Help: touch and keys", "Next view", "Quit"});
      // the press that closed the menu can arrive again underneath
      REPEAT kk := GETKEY; UNTIL kk == -1;
      b3_Drain();
      IF b3C == 1 THEN b3_Help(); END;
      IF b3C == 2 THEN b3Act := 5; END;
      IF b3C == 3 THEN b3Act := 6; END;
    END;

    // ---- do it ----
    IF b3Act >= 1 AND b3Act <= 4 THEN b3Sel := b3Act; END;
    IF b3Act == 11 THEN b3Sel := (b3Sel MOD 4) + 1; END;
    IF b3Act == 5 THEN showMode := (showMode+1) MOD 5; END;
    IF b3Act == 6 THEN b3Quit := 1; END;
    IF b3Act == 9 THEN b3X0 := MAX(0.05, b3X0-dX); b3Msg := 0; END;
    IF b3Act == 10 THEN b3X0 := b3X0+dX; b3Msg := 0; END;
    IF b3Act == 7 OR b3Act == 8 THEN
      IF b3Sel == 4 THEN
        // x₀ chosen: the arrows slide it, as +/- do
        IF b3Act == 7 THEN b3X0 := MAX(0.05, b3X0-dX); ELSE b3X0 := b3X0+dX; END;
        b3Msg := 0;
      ELSE
        // C, K or S: keep the curve as it is now for the ghost.  C and K
        // step by 0.25; a step DOWN that would take K×C below 0.5 is
        // refused and row 2 says so (below that the 0.995 quantile runs
        // to thousands of S and the plot has nothing useful to show).
        // The floor is on the PRODUCT, so C can go down to 0.25 when K
        // is large enough, and vice versa.  S scales by x1.25.
        b3Gh := b3Pdf; b3GhDx := dx; b3GhOn := 1; b3Msg := 0;
        IF b3Sel == 1 THEN
          IF b3Act == 8 THEN
            C := C+0.25;
          ELSE
            b3New := C - 0.25;
            IF b3New*K >= 0.5 THEN C := b3New; ELSE b3GhOn := 0; b3Msg := 2; END;
          END;
        END;
        IF b3Sel == 2 THEN
          IF b3Act == 8 THEN
            K := K+0.25;
          ELSE
            b3New := K - 0.25;
            IF b3New*C >= 0.5 THEN K := b3New; ELSE b3GhOn := 0; b3Msg := 2; END;
          END;
        END;
        IF b3Sel == 3 THEN
          IF b3Act == 8 THEN S := S*1.25; ELSE S := MAX(0.01, S/1.25); END;
        END;
        b3Re := 1;
      END;
    END;
    b3Act := 0;
  UNTIL b3Quit == 1;

  // ---- export the final state so it can be read in Home ----
  EP_BURR_MEAN := mmean;
  EP_BURR_SD   := msd;
  EP_BURR_PDF  := b3_bpdf(b3X0,C,K,S);
  EP_BURR_CDF  := b3_bcdf(b3X0,C,K,S);   // v2.4b: in logs (b3_bcdf)
  EP_BURR_X0   := b3X0;

  // v2.4b: x₀ rounded first (b3_Fx).  A plain x₀ goes inside P( ) as
  // before; one too small for two decimals keeps its name there and its
  // size after the area: "P(X ≤ x₀) < 0.0001 with x₀ < 0.01".
  b3XS := b3_Fx(b3X0,2); b3XT := "";
  IF b3_Eq(b3XS) <> " = " + b3XS THEN
    b3XT := " with x₀ " + b3XS;
    b3XS := "x₀";
  END;
  // v2.4a: b3_Eq, and an exact 0 read as "< 0.0001", as on the status row.
  // v2.4b: C, K and S through b3_Fx and b3_Eq ("S = 0.01", "C < 0.01")
  RETURN "Burr3: C" + b3_Eq(b3_Fx(C,2)) + ", K" + b3_Eq(b3_Fx(K,2)) + ", S" +
         b3_Eq(b3_Fx(S,2)) + ", P(X ≤ " + b3XS + ")" +
         b3_Eq(IFTE(EP_BURR_CDF > 0, b3_fmtp(EP_BURR_CDF,4), "< 0.0001")) + b3XT + "; values in EP_BURR_*";
END;
